viainti-chart 1.0.0

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.cjs ADDED
@@ -0,0 +1,3811 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var framerMotion = require('framer-motion');
5
+
6
+ const Chart = ({ data, width = 800, height = 400 }) => {
7
+ const canvasRef = React.useRef(null);
8
+ React.useEffect(() => {
9
+ const canvas = canvasRef.current;
10
+ if (!canvas)
11
+ return;
12
+ const ctx = canvas.getContext('2d');
13
+ if (!ctx)
14
+ return;
15
+ // Clear canvas
16
+ ctx.clearRect(0, 0, width, height);
17
+ if (data.length === 0)
18
+ return;
19
+ // Find min and max values
20
+ const highs = data.map((d) => d.high);
21
+ const lows = data.map((d) => d.low);
22
+ const minPrice = Math.min(...lows);
23
+ const maxPrice = Math.max(...highs);
24
+ const priceRange = maxPrice - minPrice;
25
+ const candleWidth = width / data.length;
26
+ data.forEach((candle, index) => {
27
+ const x = index * candleWidth;
28
+ const yHigh = ((maxPrice - candle.high) / priceRange) * height;
29
+ const yLow = ((maxPrice - candle.low) / priceRange) * height;
30
+ const yOpen = ((maxPrice - candle.open) / priceRange) * height;
31
+ const yClose = ((maxPrice - candle.close) / priceRange) * height;
32
+ // Draw high-low line
33
+ ctx.strokeStyle = 'black';
34
+ ctx.beginPath();
35
+ ctx.moveTo(x + candleWidth / 2, yHigh);
36
+ ctx.lineTo(x + candleWidth / 2, yLow);
37
+ ctx.stroke();
38
+ // Draw open-close body
39
+ const bodyHeight = Math.abs(yClose - yOpen);
40
+ const bodyY = Math.min(yOpen, yClose);
41
+ const isGreen = candle.close > candle.open;
42
+ ctx.fillStyle = isGreen ? 'green' : 'red';
43
+ ctx.fillRect(x + candleWidth * 0.1, bodyY, candleWidth * 0.8, bodyHeight);
44
+ // Draw open/close wicks if body is small
45
+ if (bodyHeight < 1) {
46
+ ctx.strokeStyle = isGreen ? 'green' : 'red';
47
+ ctx.beginPath();
48
+ ctx.moveTo(x + candleWidth / 2, yOpen);
49
+ ctx.lineTo(x + candleWidth / 2, yClose);
50
+ ctx.stroke();
51
+ }
52
+ });
53
+ }, [data, width, height]);
54
+ return React.createElement("canvas", { ref: canvasRef, width: width, height: height });
55
+ };
56
+
57
+ var DefaultContext = {
58
+ color: undefined,
59
+ size: undefined,
60
+ className: undefined,
61
+ style: undefined,
62
+ attr: undefined
63
+ };
64
+ var IconContext = React.createContext && /*#__PURE__*/React.createContext(DefaultContext);
65
+
66
+ var _excluded = ["attr", "size", "title"];
67
+ function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
68
+ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } } return target; }
69
+ function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
70
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
71
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
72
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
73
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
74
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
75
+ function Tree2Element(tree) {
76
+ return tree && tree.map((node, i) => /*#__PURE__*/React.createElement(node.tag, _objectSpread({
77
+ key: i
78
+ }, node.attr), Tree2Element(node.child)));
79
+ }
80
+ function GenIcon(data) {
81
+ return props => /*#__PURE__*/React.createElement(IconBase, _extends({
82
+ attr: _objectSpread({}, data.attr)
83
+ }, props), Tree2Element(data.child));
84
+ }
85
+ function IconBase(props) {
86
+ var elem = conf => {
87
+ var {
88
+ attr,
89
+ size,
90
+ title
91
+ } = props,
92
+ svgProps = _objectWithoutProperties(props, _excluded);
93
+ var computedSize = size || conf.size || "1em";
94
+ var className;
95
+ if (conf.className) className = conf.className;
96
+ if (props.className) className = (className ? className + " " : "") + props.className;
97
+ return /*#__PURE__*/React.createElement("svg", _extends({
98
+ stroke: "currentColor",
99
+ fill: "currentColor",
100
+ strokeWidth: "0"
101
+ }, conf.attr, attr, svgProps, {
102
+ className: className,
103
+ style: _objectSpread(_objectSpread({
104
+ color: props.color || conf.color
105
+ }, conf.style), props.style),
106
+ height: computedSize,
107
+ width: computedSize,
108
+ xmlns: "http://www.w3.org/2000/svg"
109
+ }), title && /*#__PURE__*/React.createElement("title", null, title), props.children);
110
+ };
111
+ return IconContext !== undefined ? /*#__PURE__*/React.createElement(IconContext.Consumer, null, conf => elem(conf)) : elem(DefaultContext);
112
+ }
113
+
114
+ // THIS FILE IS AUTO GENERATED
115
+ function BsArrowRepeat (props) {
116
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41m-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9"},"child":[]},{"tag":"path","attr":{"fillRule":"evenodd","d":"M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5 5 0 0 0 8 3M3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9z"},"child":[]}]})(props);
117
+ }function BsArrowsAngleExpand (props) {
118
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707m4.344-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707"},"child":[]}]})(props);
119
+ }function BsArrowsFullscreen (props) {
120
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707m4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707m0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707m-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707"},"child":[]}]})(props);
121
+ }function BsBarChartSteps (props) {
122
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M.5 0a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 .5 0M2 1.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-4a.5.5 0 0 1-.5-.5zm2 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5zm2 4a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5zm2 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5z"},"child":[]}]})(props);
123
+ }function BsBarChart (props) {
124
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M4 11H2v3h2zm5-4H7v7h2zm5-5v12h-2V2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"},"child":[]}]})(props);
125
+ }function BsBoundingBoxCircles (props) {
126
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M2 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2M0 2a2 2 0 0 1 3.937-.5h8.126A2 2 0 1 1 14.5 3.937v8.126a2 2 0 1 1-2.437 2.437H3.937A2 2 0 1 1 1.5 12.063V3.937A2 2 0 0 1 0 2m2.5 1.937v8.126c.703.18 1.256.734 1.437 1.437h8.126a2 2 0 0 1 1.437-1.437V3.937A2 2 0 0 1 12.063 2.5H3.937A2 2 0 0 1 2.5 3.937M14 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2M2 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m12 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2"},"child":[]}]})(props);
127
+ }function BsCamera (props) {
128
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z"},"child":[]},{"tag":"path","attr":{"d":"M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"},"child":[]}]})(props);
129
+ }function BsCheck2 (props) {
130
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0"},"child":[]}]})(props);
131
+ }function BsChevronBarExpand (props) {
132
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M3.646 10.146a.5.5 0 0 1 .708 0L8 13.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708m0-4.292a.5.5 0 0 0 .708 0L8 2.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708M1 8a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13A.5.5 0 0 1 1 8"},"child":[]}]})(props);
133
+ }function BsClockHistory (props) {
134
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022zm2.004.45a7 7 0 0 0-.985-.299l.219-.976q.576.129 1.126.342zm1.37.71a7 7 0 0 0-.439-.27l.493-.87a8 8 0 0 1 .979.654l-.615.789a7 7 0 0 0-.418-.302zm1.834 1.79a7 7 0 0 0-.653-.796l.724-.69q.406.429.747.91zm.744 1.352a7 7 0 0 0-.214-.468l.893-.45a8 8 0 0 1 .45 1.088l-.95.313a7 7 0 0 0-.179-.483m.53 2.507a7 7 0 0 0-.1-1.025l.985-.17q.1.58.116 1.17zm-.131 1.538q.05-.254.081-.51l.993.123a8 8 0 0 1-.23 1.155l-.964-.267q.069-.247.12-.501m-.952 2.379q.276-.436.486-.908l.914.405q-.24.54-.555 1.038zm-.964 1.205q.183-.183.35-.378l.758.653a8 8 0 0 1-.401.432z"},"child":[]},{"tag":"path","attr":{"d":"M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0z"},"child":[]},{"tag":"path","attr":{"d":"M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5"},"child":[]}]})(props);
135
+ }function BsCollection (props) {
136
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M2.5 3.5a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1zm2-2a.5.5 0 0 1 0-1h7a.5.5 0 0 1 0 1zM0 13a1.5 1.5 0 0 0 1.5 1.5h13A1.5 1.5 0 0 0 16 13V6a1.5 1.5 0 0 0-1.5-1.5h-13A1.5 1.5 0 0 0 0 6zm1.5.5A.5.5 0 0 1 1 13V6a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5z"},"child":[]}]})(props);
137
+ }function BsCursor (props) {
138
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103zM2.25 8.184l3.897 1.67a.5.5 0 0 1 .262.263l1.67 3.897L12.743 3.52z"},"child":[]}]})(props);
139
+ }function BsDash (props) {
140
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8"},"child":[]}]})(props);
141
+ }function BsDiagram3 (props) {
142
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5zM8.5 5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5zM0 11.5A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm4.5.5A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm4.5.5a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"},"child":[]}]})(props);
143
+ }function BsEmojiSmile (props) {
144
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M4.285 9.567a.5.5 0 0 1 .683.183A3.5 3.5 0 0 0 8 11.5a3.5 3.5 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.5 4.5 0 0 1 8 12.5a4.5 4.5 0 0 1-3.898-2.25.5.5 0 0 1 .183-.683M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5m4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5"},"child":[]}]})(props);
145
+ }function BsEyeSlash (props) {
146
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7 7 0 0 0-2.79.588l.77.771A6 6 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755q-.247.248-.517.486z"},"child":[]},{"tag":"path","attr":{"d":"M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829"},"child":[]},{"tag":"path","attr":{"d":"M3.35 5.47q-.27.24-.518.487A13 13 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7 7 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12z"},"child":[]}]})(props);
147
+ }function BsGear (props) {
148
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"},"child":[]},{"tag":"path","attr":{"d":"M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"},"child":[]}]})(props);
149
+ }function BsGraphDown (props) {
150
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M0 0h1v15h15v1H0zm14.817 11.887a.5.5 0 0 0 .07-.704l-4.5-5.5a.5.5 0 0 0-.74-.037L7.06 8.233 3.404 3.206a.5.5 0 0 0-.808.588l4 5.5a.5.5 0 0 0 .758.06l2.609-2.61 4.15 5.073a.5.5 0 0 0 .704.07"},"child":[]}]})(props);
151
+ }function BsGraphUp (props) {
152
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M0 0h1v15h15v1H0zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07"},"child":[]}]})(props);
153
+ }function BsInfoCircle (props) {
154
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"},"child":[]}]})(props);
155
+ }function BsLock (props) {
156
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1"},"child":[]}]})(props);
157
+ }function BsMagnet (props) {
158
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 1a7 7 0 0 0-7 7v3h4V8a3 3 0 0 1 6 0v3h4V8a7 7 0 0 0-7-7m7 11h-4v3h4zM5 12H1v3h4zM0 8a8 8 0 1 1 16 0v8h-6V8a2 2 0 1 0-4 0v8H0z"},"child":[]}]})(props);
159
+ }function BsPencil (props) {
160
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"},"child":[]}]})(props);
161
+ }function BsQuestionCircle (props) {
162
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286m1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94"},"child":[]}]})(props);
163
+ }function BsSlash (props) {
164
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M11.354 4.646a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708l6-6a.5.5 0 0 1 .708 0"},"child":[]}]})(props);
165
+ }function BsSquare (props) {
166
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"},"child":[]}]})(props);
167
+ }function BsTrash (props) {
168
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"},"child":[]},{"tag":"path","attr":{"d":"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"},"child":[]}]})(props);
169
+ }function BsTriangle (props) {
170
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z"},"child":[]}]})(props);
171
+ }function BsType (props) {
172
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"m2.244 13.081.943-2.803H6.66l.944 2.803H8.86L5.54 3.75H4.322L1 13.081zm2.7-7.923L6.34 9.314H3.51l1.4-4.156zm9.146 7.027h.035v.896h1.128V8.125c0-1.51-1.114-2.345-2.646-2.345-1.736 0-2.59.916-2.666 2.174h1.108c.068-.718.595-1.19 1.517-1.19.971 0 1.518.52 1.518 1.464v.731H12.19c-1.647.007-2.522.8-2.522 2.058 0 1.319.957 2.18 2.345 2.18 1.06 0 1.716-.43 2.078-1.011zm-1.763.035c-.752 0-1.456-.397-1.456-1.244 0-.65.424-1.115 1.408-1.115h1.805v.834c0 .896-.752 1.525-1.757 1.525"},"child":[]}]})(props);
173
+ }function BsZoomIn (props) {
174
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11M13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0"},"child":[]},{"tag":"path","attr":{"d":"M10.344 11.742q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1 6.5 6.5 0 0 1-1.398 1.4z"},"child":[]},{"tag":"path","attr":{"fillRule":"evenodd","d":"M6.5 3a.5.5 0 0 1 .5.5V6h2.5a.5.5 0 0 1 0 1H7v2.5a.5.5 0 0 1-1 0V7H3.5a.5.5 0 0 1 0-1H6V3.5a.5.5 0 0 1 .5-.5"},"child":[]}]})(props);
175
+ }function BsZoomOut (props) {
176
+ return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11M13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0"},"child":[]},{"tag":"path","attr":{"d":"M10.344 11.742q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1 6.5 6.5 0 0 1-1.398 1.4z"},"child":[]},{"tag":"path","attr":{"fillRule":"evenodd","d":"M3 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5"},"child":[]}]})(props);
177
+ }
178
+
179
+ const createCoordinateSystem = () => ({
180
+ pixelToPrice: (pixelY, chartHeight, minPrice, maxPrice) => {
181
+ const priceRange = maxPrice - minPrice;
182
+ return maxPrice - (pixelY / chartHeight) * priceRange;
183
+ },
184
+ priceToPixel: (price, chartHeight, minPrice, maxPrice) => {
185
+ const priceRange = maxPrice - minPrice;
186
+ return ((maxPrice - price) / priceRange) * chartHeight;
187
+ },
188
+ pixelToTime: (pixelX, chartWidth, dataLength) => {
189
+ return Math.floor((pixelX / chartWidth) * dataLength);
190
+ },
191
+ timeToPixel: (timeIndex, chartWidth, dataLength) => {
192
+ return (timeIndex / dataLength) * chartWidth;
193
+ },
194
+ snapToCandle: (pixelX, chartWidth, dataLength) => {
195
+ const candleIndex = Math.round((pixelX / chartWidth) * dataLength);
196
+ return Math.max(0, Math.min(dataLength - 1, candleIndex));
197
+ },
198
+ snapToPrice: (pixelY, chartHeight, minPrice, maxPrice, step = 0.01) => {
199
+ const price = maxPrice - (pixelY / chartHeight) * (maxPrice - minPrice);
200
+ return Math.round(price / step) * step;
201
+ }
202
+ });
203
+ const getDataBounds = (data) => {
204
+ if (!data.length)
205
+ return { minPrice: 0, maxPrice: 0, minVolume: 0, maxVolume: 0 };
206
+ const prices = data.flatMap(d => [d.open, d.high, d.low, d.close]);
207
+ const volumes = data.map(d => d.volume || 0);
208
+ return {
209
+ minPrice: Math.min(...prices),
210
+ maxPrice: Math.max(...prices),
211
+ minVolume: Math.min(...volumes),
212
+ maxVolume: Math.max(...volumes)
213
+ };
214
+ };
215
+
216
+ class Item {
217
+ constructor(data, prev, next) {
218
+ this.next = next;
219
+ if (next)
220
+ next.prev = this;
221
+ this.prev = prev;
222
+ if (prev)
223
+ prev.next = this;
224
+ this.data = data;
225
+ }
226
+ }
227
+ class LinkedList {
228
+ constructor() {
229
+ this._length = 0;
230
+ }
231
+ get head() {
232
+ return this._head && this._head.data;
233
+ }
234
+ get tail() {
235
+ return this._tail && this._tail.data;
236
+ }
237
+ get current() {
238
+ return this._current && this._current.data;
239
+ }
240
+ get length() {
241
+ return this._length;
242
+ }
243
+ push(data) {
244
+ this._tail = new Item(data, this._tail);
245
+ if (this._length === 0) {
246
+ this._head = this._tail;
247
+ this._current = this._head;
248
+ this._next = this._head;
249
+ }
250
+ this._length++;
251
+ }
252
+ pop() {
253
+ var tail = this._tail;
254
+ if (this._length === 0) {
255
+ return;
256
+ }
257
+ this._length--;
258
+ if (this._length === 0) {
259
+ this._head = this._tail = this._current = this._next = undefined;
260
+ return tail.data;
261
+ }
262
+ this._tail = tail.prev;
263
+ this._tail.next = undefined;
264
+ if (this._current === tail) {
265
+ this._current = this._tail;
266
+ this._next = undefined;
267
+ }
268
+ return tail.data;
269
+ }
270
+ shift() {
271
+ var head = this._head;
272
+ if (this._length === 0) {
273
+ return;
274
+ }
275
+ this._length--;
276
+ if (this._length === 0) {
277
+ this._head = this._tail = this._current = this._next = undefined;
278
+ return head.data;
279
+ }
280
+ this._head = this._head.next;
281
+ if (this._current === head) {
282
+ this._current = this._head;
283
+ this._next = this._current.next;
284
+ }
285
+ return head.data;
286
+ }
287
+ unshift(data) {
288
+ this._head = new Item(data, undefined, this._head);
289
+ if (this._length === 0) {
290
+ this._tail = this._head;
291
+ this._next = this._head;
292
+ }
293
+ this._length++;
294
+ }
295
+ unshiftCurrent() {
296
+ var current = this._current;
297
+ if (current === this._head || this._length < 2) {
298
+ return current && current.data;
299
+ }
300
+ // remove
301
+ if (current === this._tail) {
302
+ this._tail = current.prev;
303
+ this._tail.next = undefined;
304
+ this._current = this._tail;
305
+ }
306
+ else {
307
+ current.next.prev = current.prev;
308
+ current.prev.next = current.next;
309
+ this._current = current.prev;
310
+ }
311
+ this._next = this._current.next;
312
+ // unshift
313
+ current.next = this._head;
314
+ current.prev = undefined;
315
+ this._head.prev = current;
316
+ this._head = current;
317
+ return current.data;
318
+ }
319
+ removeCurrent() {
320
+ var current = this._current;
321
+ if (this._length === 0) {
322
+ return;
323
+ }
324
+ this._length--;
325
+ if (this._length === 0) {
326
+ this._head = this._tail = this._current = this._next = undefined;
327
+ return current.data;
328
+ }
329
+ if (current === this._tail) {
330
+ this._tail = current.prev;
331
+ this._tail.next = undefined;
332
+ this._current = this._tail;
333
+ }
334
+ else if (current === this._head) {
335
+ this._head = current.next;
336
+ this._head.prev = undefined;
337
+ this._current = this._head;
338
+ }
339
+ else {
340
+ current.next.prev = current.prev;
341
+ current.prev.next = current.next;
342
+ this._current = current.prev;
343
+ }
344
+ this._next = this._current.next;
345
+ return current.data;
346
+ }
347
+ resetCursor() {
348
+ this._current = this._next = this._head;
349
+ return this;
350
+ }
351
+ next() {
352
+ var next = this._next;
353
+ if (next !== undefined) {
354
+ this._next = next.next;
355
+ this._current = next;
356
+ return next.data;
357
+ }
358
+ }
359
+ }
360
+
361
+ let config = {};
362
+ function getConfig(key) {
363
+ return config[key];
364
+ }
365
+
366
+ function format(v) {
367
+ let precision = getConfig('precision');
368
+ if (precision) {
369
+ return parseFloat(v.toPrecision(precision));
370
+ }
371
+ return v;
372
+ }
373
+
374
+ class Indicator {
375
+ constructor(input) {
376
+ this.format = input.format || format;
377
+ }
378
+ static reverseInputs(input) {
379
+ if (input.reversedInput) {
380
+ input.values ? input.values.reverse() : undefined;
381
+ input.open ? input.open.reverse() : undefined;
382
+ input.high ? input.high.reverse() : undefined;
383
+ input.low ? input.low.reverse() : undefined;
384
+ input.close ? input.close.reverse() : undefined;
385
+ input.volume ? input.volume.reverse() : undefined;
386
+ input.timestamp ? input.timestamp.reverse() : undefined;
387
+ }
388
+ }
389
+ getResult() {
390
+ return this.result;
391
+ }
392
+ }
393
+
394
+ //STEP 1. Import Necessary indicator or rather last step
395
+ //STEP3. Add class based syntax with export
396
+ class SMA extends Indicator {
397
+ constructor(input) {
398
+ super(input);
399
+ this.period = input.period;
400
+ this.price = input.values;
401
+ var genFn = (function* (period) {
402
+ var list = new LinkedList();
403
+ var sum = 0;
404
+ var counter = 1;
405
+ var current = yield;
406
+ var result;
407
+ list.push(0);
408
+ while (true) {
409
+ if (counter < period) {
410
+ counter++;
411
+ list.push(current);
412
+ sum = sum + current;
413
+ }
414
+ else {
415
+ sum = sum - list.shift() + current;
416
+ result = ((sum) / period);
417
+ list.push(current);
418
+ }
419
+ current = yield result;
420
+ }
421
+ });
422
+ this.generator = genFn(this.period);
423
+ this.generator.next();
424
+ this.result = [];
425
+ this.price.forEach((tick) => {
426
+ var result = this.generator.next(tick);
427
+ if (result.value !== undefined) {
428
+ this.result.push(this.format(result.value));
429
+ }
430
+ });
431
+ }
432
+ nextValue(price) {
433
+ var result = this.generator.next(price).value;
434
+ if (result != undefined)
435
+ return this.format(result);
436
+ }
437
+ ;
438
+ }
439
+ SMA.calculate = sma;
440
+ function sma(input) {
441
+ Indicator.reverseInputs(input);
442
+ var result = new SMA(input).result;
443
+ if (input.reversedInput) {
444
+ result.reverse();
445
+ }
446
+ Indicator.reverseInputs(input);
447
+ return result;
448
+ }
449
+ //STEP 6. Run the tests
450
+
451
+ class EMA extends Indicator {
452
+ constructor(input) {
453
+ super(input);
454
+ var period = input.period;
455
+ var priceArray = input.values;
456
+ var exponent = (2 / (period + 1));
457
+ var sma;
458
+ this.result = [];
459
+ sma = new SMA({ period: period, values: [] });
460
+ var genFn = (function* () {
461
+ var tick = yield;
462
+ var prevEma;
463
+ while (true) {
464
+ if (prevEma !== undefined && tick !== undefined) {
465
+ prevEma = ((tick - prevEma) * exponent) + prevEma;
466
+ tick = yield prevEma;
467
+ }
468
+ else {
469
+ tick = yield;
470
+ prevEma = sma.nextValue(tick);
471
+ if (prevEma)
472
+ tick = yield prevEma;
473
+ }
474
+ }
475
+ });
476
+ this.generator = genFn();
477
+ this.generator.next();
478
+ this.generator.next();
479
+ priceArray.forEach((tick) => {
480
+ var result = this.generator.next(tick);
481
+ if (result.value != undefined) {
482
+ this.result.push(this.format(result.value));
483
+ }
484
+ });
485
+ }
486
+ nextValue(price) {
487
+ var result = this.generator.next(price).value;
488
+ if (result != undefined)
489
+ return this.format(result);
490
+ }
491
+ ;
492
+ }
493
+ EMA.calculate = ema;
494
+ function ema(input) {
495
+ Indicator.reverseInputs(input);
496
+ var result = new EMA(input).result;
497
+ if (input.reversedInput) {
498
+ result.reverse();
499
+ }
500
+ Indicator.reverseInputs(input);
501
+ return result;
502
+ }
503
+
504
+ /**
505
+ * Created by AAravindan on 5/4/16.
506
+ */
507
+ class MACD extends Indicator {
508
+ constructor(input) {
509
+ super(input);
510
+ var oscillatorMAtype = input.SimpleMAOscillator ? SMA : EMA;
511
+ var signalMAtype = input.SimpleMASignal ? SMA : EMA;
512
+ var fastMAProducer = new oscillatorMAtype({ period: input.fastPeriod, values: [], format: (v) => { return v; } });
513
+ var slowMAProducer = new oscillatorMAtype({ period: input.slowPeriod, values: [], format: (v) => { return v; } });
514
+ var signalMAProducer = new signalMAtype({ period: input.signalPeriod, values: [], format: (v) => { return v; } });
515
+ var format = this.format;
516
+ this.result = [];
517
+ this.generator = (function* () {
518
+ var index = 0;
519
+ var tick;
520
+ var MACD, signal, histogram, fast, slow;
521
+ while (true) {
522
+ if (index < input.slowPeriod) {
523
+ tick = yield;
524
+ fast = fastMAProducer.nextValue(tick);
525
+ slow = slowMAProducer.nextValue(tick);
526
+ index++;
527
+ continue;
528
+ }
529
+ if (fast && slow) { //Just for typescript to be happy
530
+ MACD = fast - slow;
531
+ signal = signalMAProducer.nextValue(MACD);
532
+ }
533
+ histogram = MACD - signal;
534
+ tick = yield ({
535
+ //fast : fast,
536
+ //slow : slow,
537
+ MACD: format(MACD),
538
+ signal: signal ? format(signal) : undefined,
539
+ histogram: isNaN(histogram) ? undefined : format(histogram)
540
+ });
541
+ fast = fastMAProducer.nextValue(tick);
542
+ slow = slowMAProducer.nextValue(tick);
543
+ }
544
+ })();
545
+ this.generator.next();
546
+ input.values.forEach((tick) => {
547
+ var result = this.generator.next(tick);
548
+ if (result.value != undefined) {
549
+ this.result.push(result.value);
550
+ }
551
+ });
552
+ }
553
+ nextValue(price) {
554
+ var result = this.generator.next(price).value;
555
+ return result;
556
+ }
557
+ ;
558
+ }
559
+ MACD.calculate = macd;
560
+ function macd(input) {
561
+ Indicator.reverseInputs(input);
562
+ var result = new MACD(input).result;
563
+ if (input.reversedInput) {
564
+ result.reverse();
565
+ }
566
+ Indicator.reverseInputs(input);
567
+ return result;
568
+ }
569
+
570
+ class AverageGain extends Indicator {
571
+ constructor(input) {
572
+ super(input);
573
+ let values = input.values;
574
+ let period = input.period;
575
+ let format = this.format;
576
+ this.generator = (function* (period) {
577
+ var currentValue = yield;
578
+ var counter = 1;
579
+ var gainSum = 0;
580
+ var avgGain;
581
+ var gain;
582
+ var lastValue = currentValue;
583
+ currentValue = yield;
584
+ while (true) {
585
+ gain = currentValue - lastValue;
586
+ gain = gain > 0 ? gain : 0;
587
+ if (gain > 0) {
588
+ gainSum = gainSum + gain;
589
+ }
590
+ if (counter < period) {
591
+ counter++;
592
+ }
593
+ else if (avgGain === undefined) {
594
+ avgGain = gainSum / period;
595
+ }
596
+ else {
597
+ avgGain = ((avgGain * (period - 1)) + gain) / period;
598
+ }
599
+ lastValue = currentValue;
600
+ avgGain = (avgGain !== undefined) ? format(avgGain) : undefined;
601
+ currentValue = yield avgGain;
602
+ }
603
+ })(period);
604
+ this.generator.next();
605
+ this.result = [];
606
+ values.forEach((tick) => {
607
+ var result = this.generator.next(tick);
608
+ if (result.value !== undefined) {
609
+ this.result.push(result.value);
610
+ }
611
+ });
612
+ }
613
+ nextValue(price) {
614
+ return this.generator.next(price).value;
615
+ }
616
+ ;
617
+ }
618
+ AverageGain.calculate = averagegain;
619
+ function averagegain(input) {
620
+ Indicator.reverseInputs(input);
621
+ var result = new AverageGain(input).result;
622
+ if (input.reversedInput) {
623
+ result.reverse();
624
+ }
625
+ Indicator.reverseInputs(input);
626
+ return result;
627
+ }
628
+
629
+ class AverageLoss extends Indicator {
630
+ constructor(input) {
631
+ super(input);
632
+ let values = input.values;
633
+ let period = input.period;
634
+ let format = this.format;
635
+ this.generator = (function* (period) {
636
+ var currentValue = yield;
637
+ var counter = 1;
638
+ var lossSum = 0;
639
+ var avgLoss;
640
+ var loss;
641
+ var lastValue = currentValue;
642
+ currentValue = yield;
643
+ while (true) {
644
+ loss = lastValue - currentValue;
645
+ loss = loss > 0 ? loss : 0;
646
+ if (loss > 0) {
647
+ lossSum = lossSum + loss;
648
+ }
649
+ if (counter < period) {
650
+ counter++;
651
+ }
652
+ else if (avgLoss === undefined) {
653
+ avgLoss = lossSum / period;
654
+ }
655
+ else {
656
+ avgLoss = ((avgLoss * (period - 1)) + loss) / period;
657
+ }
658
+ lastValue = currentValue;
659
+ avgLoss = (avgLoss !== undefined) ? format(avgLoss) : undefined;
660
+ currentValue = yield avgLoss;
661
+ }
662
+ })(period);
663
+ this.generator.next();
664
+ this.result = [];
665
+ values.forEach((tick) => {
666
+ var result = this.generator.next(tick);
667
+ if (result.value !== undefined) {
668
+ this.result.push(result.value);
669
+ }
670
+ });
671
+ }
672
+ nextValue(price) {
673
+ return this.generator.next(price).value;
674
+ }
675
+ ;
676
+ }
677
+ AverageLoss.calculate = averageloss;
678
+ function averageloss(input) {
679
+ Indicator.reverseInputs(input);
680
+ var result = new AverageLoss(input).result;
681
+ if (input.reversedInput) {
682
+ result.reverse();
683
+ }
684
+ Indicator.reverseInputs(input);
685
+ return result;
686
+ }
687
+
688
+ /**
689
+ * Created by AAravindan on 5/5/16.
690
+ */
691
+ class RSI extends Indicator {
692
+ constructor(input) {
693
+ super(input);
694
+ var period = input.period;
695
+ var values = input.values;
696
+ var GainProvider = new AverageGain({ period: period, values: [] });
697
+ var LossProvider = new AverageLoss({ period: period, values: [] });
698
+ this.generator = (function* (period) {
699
+ var current = yield;
700
+ var lastAvgGain, lastAvgLoss, RS, currentRSI;
701
+ while (true) {
702
+ lastAvgGain = GainProvider.nextValue(current);
703
+ lastAvgLoss = LossProvider.nextValue(current);
704
+ if ((lastAvgGain !== undefined) && (lastAvgLoss !== undefined)) {
705
+ if (lastAvgLoss === 0) {
706
+ currentRSI = 100;
707
+ }
708
+ else if (lastAvgGain === 0) {
709
+ currentRSI = 0;
710
+ }
711
+ else {
712
+ RS = lastAvgGain / lastAvgLoss;
713
+ RS = isNaN(RS) ? 0 : RS;
714
+ currentRSI = parseFloat((100 - (100 / (1 + RS))).toFixed(2));
715
+ }
716
+ }
717
+ current = yield currentRSI;
718
+ }
719
+ })();
720
+ this.generator.next();
721
+ this.result = [];
722
+ values.forEach((tick) => {
723
+ var result = this.generator.next(tick);
724
+ if (result.value !== undefined) {
725
+ this.result.push(result.value);
726
+ }
727
+ });
728
+ }
729
+ ;
730
+ nextValue(price) {
731
+ return this.generator.next(price).value;
732
+ }
733
+ ;
734
+ }
735
+ RSI.calculate = rsi;
736
+ function rsi(input) {
737
+ Indicator.reverseInputs(input);
738
+ var result = new RSI(input).result;
739
+ if (input.reversedInput) {
740
+ result.reverse();
741
+ }
742
+ Indicator.reverseInputs(input);
743
+ return result;
744
+ }
745
+
746
+ class IndicatorCalculator {
747
+ static calculateSMA(data, period) {
748
+ const closes = data.map(d => d.close);
749
+ const smaValues = SMA.calculate({ period, values: closes });
750
+ return {
751
+ name: `SMA(${period})`,
752
+ type: 'line',
753
+ data: smaValues,
754
+ color: '#2962ff',
755
+ style: 'solid',
756
+ width: 2
757
+ };
758
+ }
759
+ static calculateEMA(data, period) {
760
+ const closes = data.map(d => d.close);
761
+ const emaValues = EMA.calculate({ period, values: closes });
762
+ return {
763
+ name: `EMA(${period})`,
764
+ type: 'line',
765
+ data: emaValues,
766
+ color: '#ff6b35',
767
+ style: 'solid',
768
+ width: 2
769
+ };
770
+ }
771
+ static calculateRSI(data, period = 14) {
772
+ const closes = data.map(d => d.close);
773
+ const rsiValues = RSI.calculate({ period, values: closes });
774
+ return {
775
+ name: `RSI(${period})`,
776
+ type: 'line',
777
+ data: rsiValues,
778
+ color: '#00d4aa',
779
+ style: 'solid',
780
+ width: 1
781
+ };
782
+ }
783
+ static calculateMACD(data, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
784
+ const closes = data.map(d => d.close);
785
+ const macdResult = MACD.calculate({
786
+ fastPeriod,
787
+ slowPeriod,
788
+ signalPeriod,
789
+ values: closes,
790
+ SimpleMAOscillator: false,
791
+ SimpleMASignal: false
792
+ });
793
+ return {
794
+ macd: {
795
+ name: `MACD(${fastPeriod},${slowPeriod},${signalPeriod})`,
796
+ type: 'line',
797
+ data: macdResult.map(d => d.MACD || 0),
798
+ color: '#2962ff',
799
+ style: 'solid',
800
+ width: 1
801
+ },
802
+ signal: {
803
+ name: 'Signal',
804
+ type: 'line',
805
+ data: macdResult.map(d => d.signal || 0),
806
+ color: '#ff6b35',
807
+ style: 'solid',
808
+ width: 1
809
+ },
810
+ histogram: {
811
+ name: 'MACD Histogram',
812
+ type: 'histogram',
813
+ data: macdResult.map(d => d.histogram || 0),
814
+ color: '#00d4aa',
815
+ width: 2
816
+ }
817
+ };
818
+ }
819
+ }
820
+
821
+ function createStore(creator) {
822
+ let state;
823
+ const listeners = new Set();
824
+ const setState = (partial) => {
825
+ const partialState = typeof partial === 'function' ? partial(state) : partial;
826
+ state = { ...state, ...partialState };
827
+ listeners.forEach((listener) => listener());
828
+ };
829
+ const getState = () => state;
830
+ state = creator(setState, getState);
831
+ const subscribe = (listener) => {
832
+ listeners.add(listener);
833
+ return () => {
834
+ listeners.delete(listener);
835
+ };
836
+ };
837
+ const useStore = () => React.useSyncExternalStore(subscribe, getState, getState);
838
+ useStore.getState = getState;
839
+ useStore.setState = setState;
840
+ return useStore;
841
+ }
842
+
843
+ const useChartStore = createStore((set) => ({
844
+ data: [],
845
+ visibleData: [],
846
+ timeframe: '1h',
847
+ zoomLevel: 1,
848
+ panOffset: 0,
849
+ indicators: [],
850
+ showIndicatorsPanel: false,
851
+ activeTool: 'cursor',
852
+ cursorType: 'cross',
853
+ strokeColor: '#8ab4ff',
854
+ strokeWidth: 1.5,
855
+ magnetEnabled: true,
856
+ drawings: [],
857
+ selectedDrawing: null,
858
+ isDrawing: false,
859
+ currentDrawing: null,
860
+ selectedEmoji: null,
861
+ interactionsLocked: false,
862
+ drawingsHidden: false,
863
+ currentPrice: 0,
864
+ priceChange: 0,
865
+ priceChangePercent: 0,
866
+ isLoading: false,
867
+ setData: (data) => {
868
+ const lastPrice = data[data.length - 1]?.close || 0;
869
+ const firstPrice = data[0]?.close || 0;
870
+ const priceChange = lastPrice - firstPrice;
871
+ const priceChangePercent = firstPrice > 0 ? (priceChange / firstPrice) * 100 : 0;
872
+ set({
873
+ data,
874
+ currentPrice: lastPrice,
875
+ priceChange,
876
+ priceChangePercent
877
+ });
878
+ },
879
+ setVisibleData: (visibleData) => set({ visibleData }),
880
+ setTimeframe: (timeframe) => set({ timeframe }),
881
+ setZoomLevel: (zoomLevel) => set({ zoomLevel }),
882
+ setPanOffset: (panOffset) => set({ panOffset }),
883
+ addIndicator: (indicator) => set((state) => ({
884
+ indicators: [...state.indicators, indicator]
885
+ })),
886
+ removeIndicator: (name) => set((state) => ({
887
+ indicators: state.indicators.filter((ind) => ind.name !== name)
888
+ })),
889
+ toggleIndicatorsPanel: () => set((state) => ({
890
+ showIndicatorsPanel: !state.showIndicatorsPanel
891
+ })),
892
+ updateCurrentPrice: (currentPrice) => set({ currentPrice }),
893
+ setLoading: (isLoading) => set({ isLoading }),
894
+ setActiveTool: (activeTool) => set({ activeTool }),
895
+ setCursorType: (cursorType) => set({ cursorType }),
896
+ setStrokeColor: (strokeColor) => set({ strokeColor }),
897
+ setStrokeWidth: (strokeWidth) => set({ strokeWidth }),
898
+ toggleMagnet: () => set((state) => ({ magnetEnabled: !state.magnetEnabled })),
899
+ addDrawing: (drawing) => set((state) => ({
900
+ drawings: [...state.drawings, drawing]
901
+ })),
902
+ updateDrawing: (id, updates) => set((state) => ({
903
+ drawings: state.drawings.map((d) => (d.id === id ? { ...d, ...updates } : d))
904
+ })),
905
+ deleteDrawing: (id) => set((state) => ({
906
+ drawings: state.drawings.filter((d) => d.id !== id),
907
+ selectedDrawing: state.selectedDrawing === id ? null : state.selectedDrawing
908
+ })),
909
+ clearDrawings: () => set({ drawings: [], selectedDrawing: null }),
910
+ selectDrawing: (selectedDrawing) => set({ selectedDrawing }),
911
+ setIsDrawing: (isDrawing) => set({ isDrawing }),
912
+ setCurrentDrawing: (currentDrawing) => set({ currentDrawing }),
913
+ setSelectedEmoji: (selectedEmoji) => set({ selectedEmoji }),
914
+ toggleInteractionsLock: () => set((state) => {
915
+ const nextLocked = !state.interactionsLocked;
916
+ return {
917
+ interactionsLocked: nextLocked,
918
+ activeTool: nextLocked ? 'cursor' : state.activeTool,
919
+ isDrawing: nextLocked ? false : state.isDrawing
920
+ };
921
+ }),
922
+ toggleDrawingsHidden: () => set((state) => ({ drawingsHidden: !state.drawingsHidden }))
923
+ }));
924
+
925
+ const EMOJIS = ['⭐', '🔥', '🚀', '✅', '❌', '💎', '🐂', '🐻', '📈', '📉'];
926
+ const cursorOptions = [
927
+ { type: 'cross', icon: React.createElement(BsCursor, null), label: 'Cross' },
928
+ { type: 'dot', icon: React.createElement("div", { style: { width: '6px', height: '6px', borderRadius: '50%', background: 'currentColor' } }), label: 'Dot' },
929
+ { type: 'arrow', icon: React.createElement(BsCursor, { style: { transform: 'rotate(45deg)' } }), label: 'Arrow' },
930
+ { type: 'eraser', icon: React.createElement(BsTrash, { style: { fontSize: '14px' } }), label: 'Eraser' }
931
+ ];
932
+ const lineOptions = [
933
+ { tool: 'trendline', icon: React.createElement(BsSlash, null), label: 'Trend line' },
934
+ { tool: 'ray', icon: React.createElement(BsSlash, { style: { transform: 'translateX(-3px)' } }), label: 'Ray' },
935
+ { tool: 'info-line', icon: React.createElement(BsGraphUp, null), label: 'Info line' },
936
+ { tool: 'extended-line', icon: React.createElement(BsSlash, { style: { width: '24px' } }), label: 'Extended line' },
937
+ { tool: 'trend-angle', icon: React.createElement(BsSlash, { style: { transform: 'rotate(15deg)' } }), label: 'Trend angle' },
938
+ { tool: 'horizontal', icon: React.createElement(BsDash, { style: { width: '22px' } }), label: 'Horizontal segment' },
939
+ { tool: 'horizontal-line', icon: React.createElement(BsDash, null), label: 'Horizontal line' },
940
+ { tool: 'horizontal-ray', icon: React.createElement(BsDash, { style: { transform: 'translateX(4px)' } }), label: 'Horizontal ray' },
941
+ { tool: 'vertical-line', icon: React.createElement(BsDash, { style: { transform: 'rotate(90deg)' } }), label: 'Vertical line' },
942
+ { tool: 'cross-line', icon: React.createElement("span", { style: { fontWeight: 700 } }, "\u271A"), label: 'Cross line' }
943
+ ];
944
+ const channelOptions = [
945
+ { tool: 'parallel', icon: React.createElement(BsArrowsAngleExpand, null), label: 'Parallel channel' },
946
+ { tool: 'channel', icon: React.createElement(BsChevronBarExpand, null), label: 'Standard channel' },
947
+ { tool: 'regression-trend', icon: React.createElement(BsGraphUp, null), label: 'Regression trend' },
948
+ { tool: 'flat-top-bottom', icon: React.createElement(BsDash, { style: { width: '24px' } }), label: 'Flat top/bottom' },
949
+ { tool: 'disjoint-channel', icon: React.createElement(BsChevronBarExpand, { style: { transform: 'rotate(90deg)' } }), label: 'Disjoint channel' }
950
+ ];
951
+ const pitchforkOptions = [
952
+ { tool: 'pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Pitchfork' },
953
+ { tool: 'schiff-pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Schiff pitchfork' },
954
+ { tool: 'modified-schiff-pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Modified Schiff' },
955
+ { tool: 'inside-pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Inside pitchfork' }
956
+ ];
957
+ const shapeOptions = [
958
+ { tool: 'rectangle', icon: React.createElement(BsSquare, null), label: 'Rectángulo' },
959
+ { tool: 'triangle', icon: React.createElement(BsTriangle, null), label: 'Triángulo' }
960
+ ];
961
+ const measurementOptions = [
962
+ { tool: 'fibonacci', icon: React.createElement(BsGraphUp, null), label: 'Fibonacci' },
963
+ { tool: 'ruler', icon: React.createElement(BsGraphDown, null), label: 'Regla' }
964
+ ];
965
+ const popoverStyle = {
966
+ position: 'absolute',
967
+ left: '64px',
968
+ top: '50%',
969
+ transform: 'translateY(-50%)',
970
+ background: '#020617',
971
+ border: '1px solid #1f2937',
972
+ borderRadius: '14px',
973
+ padding: '10px',
974
+ boxShadow: '0 12px 30px rgba(0,0,0,0.5)',
975
+ display: 'grid',
976
+ gap: '8px',
977
+ zIndex: 1100
978
+ };
979
+ const DrawingToolbar = () => {
980
+ const { activeTool, cursorType, strokeColor, strokeWidth, setActiveTool, setCursorType, setStrokeColor, setStrokeWidth, clearDrawings, setSelectedEmoji, setIsDrawing } = useChartStore();
981
+ const [openMenu, setOpenMenu] = React.useState(null);
982
+ const [showEmojiPicker, setShowEmojiPicker] = React.useState(false);
983
+ const LINE_COLORS = ['#8ab4ff', '#f472b6', '#34d399', '#facc15', '#f87171', '#e2e8f0'];
984
+ const WIDTH_OPTIONS = [1, 1.5, 2.5, 3.5];
985
+ const handleSelectTool = (tool) => {
986
+ setActiveTool(tool);
987
+ setIsDrawing(false);
988
+ setOpenMenu(null);
989
+ setShowEmojiPicker(false);
990
+ };
991
+ const handleCursorSelect = (type) => {
992
+ setCursorType(type);
993
+ setOpenMenu(null);
994
+ };
995
+ const toggleMenu = (menu) => {
996
+ setShowEmojiPicker(false);
997
+ setOpenMenu(prev => (prev === menu ? null : menu));
998
+ };
999
+ const buttonConfig = [
1000
+ {
1001
+ key: 'cursor',
1002
+ icon: React.createElement(BsCursor, null),
1003
+ active: openMenu === 'cursor',
1004
+ onClick: () => toggleMenu('cursor'),
1005
+ title: 'Cursores'
1006
+ },
1007
+ {
1008
+ key: 'lines',
1009
+ icon: React.createElement(BsSlash, null),
1010
+ active: lineOptions.some(option => option.tool === activeTool) || openMenu === 'lines',
1011
+ onClick: () => toggleMenu('lines'),
1012
+ title: 'Lines'
1013
+ },
1014
+ {
1015
+ key: 'shapes',
1016
+ icon: React.createElement(BsSquare, null),
1017
+ active: shapeOptions.some(option => option.tool === activeTool) || openMenu === 'shapes',
1018
+ onClick: () => toggleMenu('shapes'),
1019
+ title: 'Shapes'
1020
+ },
1021
+ {
1022
+ key: 'channels',
1023
+ icon: React.createElement(BsChevronBarExpand, null),
1024
+ active: channelOptions.some(option => option.tool === activeTool) || openMenu === 'channels',
1025
+ onClick: () => toggleMenu('channels'),
1026
+ title: 'Channels'
1027
+ },
1028
+ {
1029
+ key: 'pitchforks',
1030
+ icon: React.createElement(BsDiagram3, null),
1031
+ active: pitchforkOptions.some(option => option.tool === activeTool) || openMenu === 'pitchforks',
1032
+ onClick: () => toggleMenu('pitchforks'),
1033
+ title: 'Pitchforks'
1034
+ },
1035
+ {
1036
+ key: 'measurements',
1037
+ icon: React.createElement(BsGraphUp, null),
1038
+ active: measurementOptions.some(option => option.tool === activeTool) || openMenu === 'measurements',
1039
+ onClick: () => toggleMenu('measurements'),
1040
+ title: 'Mediciones'
1041
+ },
1042
+ {
1043
+ key: 'brush',
1044
+ icon: React.createElement(BsPencil, null),
1045
+ active: activeTool === 'freehand',
1046
+ onClick: () => handleSelectTool('freehand'),
1047
+ title: 'Brush'
1048
+ },
1049
+ {
1050
+ key: 'text',
1051
+ icon: React.createElement(BsType, null),
1052
+ active: activeTool === 'text',
1053
+ onClick: () => handleSelectTool('text'),
1054
+ title: 'Texto y notas'
1055
+ },
1056
+ {
1057
+ key: 'emoji',
1058
+ icon: React.createElement(BsEmojiSmile, null),
1059
+ active: showEmojiPicker,
1060
+ onClick: () => {
1061
+ setOpenMenu(null);
1062
+ setShowEmojiPicker(prev => !prev);
1063
+ },
1064
+ title: 'Emojis'
1065
+ },
1066
+ {
1067
+ key: 'clear',
1068
+ icon: React.createElement(BsTrash, null),
1069
+ active: false,
1070
+ onClick: () => {
1071
+ clearDrawings();
1072
+ setOpenMenu(null);
1073
+ setShowEmojiPicker(false);
1074
+ },
1075
+ title: 'Limpiar todo'
1076
+ }
1077
+ ];
1078
+ return (React.createElement("div", { style: { position: 'relative', height: '100%' } },
1079
+ React.createElement("div", { style: {
1080
+ height: '100%',
1081
+ display: 'flex',
1082
+ flexDirection: 'column',
1083
+ alignItems: 'center',
1084
+ gap: '12px'
1085
+ } }, buttonConfig.map((btn) => (React.createElement("div", { key: btn.key, style: { position: 'relative' } },
1086
+ React.createElement("button", { onClick: btn.onClick, title: btn.title, "aria-label": btn.title, style: {
1087
+ width: '46px',
1088
+ height: '46px',
1089
+ borderRadius: '14px',
1090
+ border: `1px solid ${btn.active ? '#2563eb' : '#1f2937'}`,
1091
+ background: btn.active ? 'rgba(37,99,235,0.25)' : 'rgba(15,23,42,0.4)',
1092
+ color: btn.active ? '#f8fafc' : '#b2b5be',
1093
+ cursor: 'pointer',
1094
+ display: 'flex',
1095
+ alignItems: 'center',
1096
+ justifyContent: 'center',
1097
+ fontSize: '18px',
1098
+ transition: 'all 0.2s'
1099
+ } }, btn.icon),
1100
+ btn.key === 'cursor' && openMenu === 'cursor' && (React.createElement("div", { style: { ...popoverStyle, gridTemplateColumns: 'repeat(2, 1fr)', width: '200px' } }, cursorOptions.map(option => (React.createElement("button", { key: option.type, onClick: () => handleCursorSelect(option.type), style: {
1101
+ width: '84px',
1102
+ height: '60px',
1103
+ borderRadius: '12px',
1104
+ border: `1px solid ${cursorType === option.type ? '#2563eb' : '#1f2937'}`,
1105
+ background: cursorType === option.type ? 'rgba(37,99,235,0.25)' : 'rgba(2,6,23,0.6)',
1106
+ color: '#e2e8f0',
1107
+ display: 'flex',
1108
+ flexDirection: 'column',
1109
+ alignItems: 'center',
1110
+ justifyContent: 'center',
1111
+ gap: '4px',
1112
+ cursor: 'pointer'
1113
+ }, title: option.label },
1114
+ React.createElement("span", { style: { fontSize: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, option.icon),
1115
+ React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase' } }, option.label)))))),
1116
+ btn.key === 'lines' && openMenu === 'lines' && (React.createElement("div", { style: { ...popoverStyle, width: '280px' } },
1117
+ React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Lines"),
1118
+ React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, lineOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
1119
+ borderRadius: '12px',
1120
+ border: `1px solid ${activeTool === option.tool ? '#2563eb' : '#1f2937'}`,
1121
+ background: activeTool === option.tool ? 'rgba(37,99,235,0.2)' : 'rgba(2,6,23,0.6)',
1122
+ color: '#e2e8f0',
1123
+ display: 'flex',
1124
+ flexDirection: 'column',
1125
+ alignItems: 'center',
1126
+ justifyContent: 'center',
1127
+ padding: '10px',
1128
+ gap: '6px',
1129
+ cursor: 'pointer'
1130
+ }, title: option.label },
1131
+ React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
1132
+ React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))),
1133
+ React.createElement("div", { style: { marginTop: '12px' } },
1134
+ React.createElement("p", { style: { margin: '0 0 6px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Stroke color"),
1135
+ React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '6px' } }, LINE_COLORS.map(color => (React.createElement("button", { key: color, onClick: () => setStrokeColor(color), style: {
1136
+ width: '26px',
1137
+ height: '26px',
1138
+ borderRadius: '50%',
1139
+ border: strokeColor === color ? '2px solid #f8fafc' : '2px solid transparent',
1140
+ background: color,
1141
+ cursor: 'pointer'
1142
+ }, "aria-label": `Stroke color ${color}` }))))),
1143
+ React.createElement("div", { style: { marginTop: '12px' } },
1144
+ React.createElement("p", { style: { margin: '0 0 6px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Stroke width"),
1145
+ React.createElement("div", { style: { display: 'flex', gap: '6px', flexWrap: 'wrap' } }, WIDTH_OPTIONS.map(width => (React.createElement("button", { key: width, onClick: () => setStrokeWidth(width), style: {
1146
+ padding: '4px 10px',
1147
+ borderRadius: '999px',
1148
+ border: strokeWidth === width ? '1px solid #2563eb' : '1px solid #1f2937',
1149
+ background: strokeWidth === width ? 'rgba(37,99,235,0.2)' : 'rgba(2,6,23,0.6)',
1150
+ color: '#e2e8f0',
1151
+ fontSize: '11px',
1152
+ cursor: 'pointer'
1153
+ } },
1154
+ width,
1155
+ "px"))))))),
1156
+ btn.key === 'shapes' && openMenu === 'shapes' && (React.createElement("div", { style: { ...popoverStyle, width: '200px' } },
1157
+ React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Shapes"),
1158
+ React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, shapeOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
1159
+ borderRadius: '12px',
1160
+ border: `1px solid ${activeTool === option.tool ? '#fcd34d' : '#1f2937'}`,
1161
+ background: activeTool === option.tool ? 'rgba(252,211,77,0.18)' : 'rgba(2,6,23,0.6)',
1162
+ color: '#e2e8f0',
1163
+ display: 'flex',
1164
+ flexDirection: 'column',
1165
+ alignItems: 'center',
1166
+ justifyContent: 'center',
1167
+ padding: '10px',
1168
+ gap: '6px',
1169
+ cursor: 'pointer'
1170
+ }, title: option.label },
1171
+ React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
1172
+ React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
1173
+ btn.key === 'channels' && openMenu === 'channels' && (React.createElement("div", { style: { ...popoverStyle, width: '220px' } },
1174
+ React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Channels"),
1175
+ React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, channelOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
1176
+ borderRadius: '12px',
1177
+ border: `1px solid ${activeTool === option.tool ? '#22d3ee' : '#1f2937'}`,
1178
+ background: activeTool === option.tool ? 'rgba(34,211,238,0.15)' : 'rgba(2,6,23,0.6)',
1179
+ color: '#e2e8f0',
1180
+ display: 'flex',
1181
+ flexDirection: 'column',
1182
+ alignItems: 'center',
1183
+ justifyContent: 'center',
1184
+ padding: '10px',
1185
+ gap: '6px',
1186
+ cursor: 'pointer'
1187
+ }, title: option.label },
1188
+ React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
1189
+ React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
1190
+ btn.key === 'pitchforks' && openMenu === 'pitchforks' && (React.createElement("div", { style: { ...popoverStyle, width: '200px' } },
1191
+ React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Pitchforks"),
1192
+ React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, pitchforkOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
1193
+ borderRadius: '12px',
1194
+ border: `1px solid ${activeTool === option.tool ? '#f472b6' : '#1f2937'}`,
1195
+ background: activeTool === option.tool ? 'rgba(244,114,182,0.15)' : 'rgba(2,6,23,0.6)',
1196
+ color: '#e2e8f0',
1197
+ display: 'flex',
1198
+ flexDirection: 'column',
1199
+ alignItems: 'center',
1200
+ justifyContent: 'center',
1201
+ padding: '10px',
1202
+ gap: '6px',
1203
+ cursor: 'pointer'
1204
+ }, title: option.label },
1205
+ React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
1206
+ React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', textAlign: 'center' } }, option.label))))))),
1207
+ btn.key === 'measurements' && openMenu === 'measurements' && (React.createElement("div", { style: { ...popoverStyle, gridTemplateColumns: 'repeat(2, 1fr)', width: '160px' } }, measurementOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
1208
+ width: '64px',
1209
+ height: '54px',
1210
+ borderRadius: '12px',
1211
+ border: `1px solid ${activeTool === option.tool ? '#fbbf24' : '#1f2937'}`,
1212
+ background: activeTool === option.tool ? 'rgba(251,191,36,0.18)' : 'rgba(2,6,23,0.6)',
1213
+ color: '#fbbf24',
1214
+ display: 'flex',
1215
+ flexDirection: 'column',
1216
+ alignItems: 'center',
1217
+ justifyContent: 'center',
1218
+ gap: '4px',
1219
+ fontSize: '10px',
1220
+ cursor: 'pointer'
1221
+ }, title: option.label },
1222
+ React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
1223
+ React.createElement("span", null, option.label)))))),
1224
+ btn.key === 'emoji' && showEmojiPicker && (React.createElement("div", { style: {
1225
+ position: 'absolute',
1226
+ left: '64px',
1227
+ top: '50%',
1228
+ transform: 'translateY(-50%)',
1229
+ zIndex: 1100,
1230
+ background: '#020617',
1231
+ borderRadius: '14px',
1232
+ overflow: 'hidden',
1233
+ boxShadow: '0 18px 40px rgba(0,0,0,0.65)',
1234
+ padding: '10px',
1235
+ border: '1px solid #1f2937'
1236
+ } },
1237
+ React.createElement("div", { style: {
1238
+ display: 'grid',
1239
+ gridTemplateColumns: 'repeat(3, 1fr)',
1240
+ gap: '6px'
1241
+ } }, EMOJIS.map((emoji) => (React.createElement("button", { key: emoji, onClick: () => {
1242
+ setSelectedEmoji(emoji);
1243
+ setActiveTool('icon');
1244
+ setShowEmojiPicker(false);
1245
+ }, style: {
1246
+ width: '40px',
1247
+ height: '40px',
1248
+ background: '#111827',
1249
+ border: '1px solid #1f2937',
1250
+ borderRadius: '10px',
1251
+ cursor: 'pointer',
1252
+ display: 'flex',
1253
+ alignItems: 'center',
1254
+ justifyContent: 'center',
1255
+ fontSize: '18px',
1256
+ color: '#f8fafc'
1257
+ } },
1258
+ React.createElement("span", null, emoji)))))))))))));
1259
+ };
1260
+
1261
+ const drawRoundedRect = (ctx, x, y, width, height, radius = 8) => {
1262
+ const r = Math.min(radius, width / 2, height / 2);
1263
+ ctx.beginPath();
1264
+ ctx.moveTo(x + r, y);
1265
+ ctx.lineTo(x + width - r, y);
1266
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
1267
+ ctx.lineTo(x + width, y + height - r);
1268
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
1269
+ ctx.lineTo(x + r, y + height);
1270
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
1271
+ ctx.lineTo(x, y + r);
1272
+ ctx.quadraticCurveTo(x, y, x + r, y);
1273
+ ctx.closePath();
1274
+ };
1275
+ class DrawingRenderer {
1276
+ static draw(drawing, ctx) {
1277
+ if (!drawing.visible)
1278
+ return;
1279
+ ctx.save();
1280
+ ctx.strokeStyle = drawing.color;
1281
+ ctx.lineWidth = drawing.width;
1282
+ ctx.setLineDash(drawing.style === 'dashed' ? [5, 5] : drawing.style === 'dotted' ? [2, 2] : []);
1283
+ switch (drawing.type) {
1284
+ case 'trendline':
1285
+ this.drawLine(drawing, ctx);
1286
+ break;
1287
+ case 'ray':
1288
+ this.drawRay(drawing, ctx);
1289
+ break;
1290
+ case 'extended-line':
1291
+ this.drawExtendedLine(drawing, ctx);
1292
+ break;
1293
+ case 'info-line':
1294
+ this.drawInfoLine(drawing, ctx);
1295
+ break;
1296
+ case 'trend-angle':
1297
+ this.drawTrendAngle(drawing, ctx);
1298
+ break;
1299
+ case 'horizontal':
1300
+ this.drawLine(drawing, ctx);
1301
+ break;
1302
+ case 'horizontal-line':
1303
+ this.drawInfiniteHorizontal(drawing, ctx);
1304
+ break;
1305
+ case 'horizontal-ray':
1306
+ this.drawHorizontalRay(drawing, ctx);
1307
+ break;
1308
+ case 'vertical-line':
1309
+ this.drawVerticalLine(drawing, ctx);
1310
+ break;
1311
+ case 'parallel':
1312
+ case 'channel':
1313
+ case 'regression-trend':
1314
+ case 'flat-top-bottom':
1315
+ case 'disjoint-channel':
1316
+ case 'pitchfork':
1317
+ case 'schiff-pitchfork':
1318
+ case 'modified-schiff-pitchfork':
1319
+ case 'inside-pitchfork':
1320
+ this.drawChannel(drawing, ctx);
1321
+ break;
1322
+ case 'rectangle':
1323
+ this.drawRectangle(drawing, ctx);
1324
+ break;
1325
+ case 'triangle':
1326
+ this.drawTriangle(drawing, ctx);
1327
+ break;
1328
+ case 'text':
1329
+ this.drawText(drawing, ctx);
1330
+ break;
1331
+ case 'icon':
1332
+ this.drawIcon(drawing, ctx);
1333
+ break;
1334
+ case 'ruler':
1335
+ this.drawRuler(drawing, ctx);
1336
+ break;
1337
+ case 'freehand':
1338
+ this.drawFreehand(drawing, ctx);
1339
+ break;
1340
+ case 'fibonacci':
1341
+ this.drawFibonacci(drawing, ctx);
1342
+ break;
1343
+ case 'cross-line':
1344
+ this.drawCrossLine(drawing, ctx);
1345
+ break;
1346
+ default:
1347
+ this.drawLine(drawing, ctx);
1348
+ }
1349
+ ctx.setLineDash([]);
1350
+ ctx.restore();
1351
+ }
1352
+ static drawLine(drawing, ctx) {
1353
+ if (drawing.points.length < 2)
1354
+ return;
1355
+ const start = drawing.points[0];
1356
+ const end = drawing.points[1];
1357
+ if (!start || !end)
1358
+ return;
1359
+ ctx.beginPath();
1360
+ ctx.moveTo(start.x, start.y);
1361
+ ctx.lineTo(end.x, end.y);
1362
+ ctx.stroke();
1363
+ if (drawing.type === 'parallel' && drawing.points.length >= 4) {
1364
+ const start2 = drawing.points[2];
1365
+ const end2 = drawing.points[3];
1366
+ if (start2 && end2) {
1367
+ ctx.beginPath();
1368
+ ctx.moveTo(start2.x, start2.y);
1369
+ ctx.lineTo(end2.x, end2.y);
1370
+ ctx.stroke();
1371
+ }
1372
+ }
1373
+ }
1374
+ static drawRay(drawing, ctx) {
1375
+ if (drawing.points.length < 2)
1376
+ return;
1377
+ const start = drawing.points[0];
1378
+ const end = drawing.points[1];
1379
+ if (!start || !end)
1380
+ return;
1381
+ const intersections = this.computeBoundaryIntersections(start, end, ctx.canvas.width, ctx.canvas.height)
1382
+ .filter(point => point.t >= 0)
1383
+ .sort((a, b) => a.t - b.t);
1384
+ const target = intersections[0];
1385
+ if (!target) {
1386
+ this.drawLine(drawing, ctx);
1387
+ return;
1388
+ }
1389
+ ctx.beginPath();
1390
+ ctx.moveTo(start.x, start.y);
1391
+ ctx.lineTo(target.x, target.y);
1392
+ ctx.stroke();
1393
+ }
1394
+ static drawExtendedLine(drawing, ctx) {
1395
+ if (drawing.points.length < 2)
1396
+ return;
1397
+ const start = drawing.points[0];
1398
+ const end = drawing.points[1];
1399
+ if (!start || !end)
1400
+ return;
1401
+ const intersections = this.computeBoundaryIntersections(start, end, ctx.canvas.width, ctx.canvas.height);
1402
+ const forward = intersections.filter(point => point.t >= 0).sort((a, b) => a.t - b.t)[0];
1403
+ const backward = intersections.filter(point => point.t <= 0).sort((a, b) => b.t - a.t)[0];
1404
+ if (!forward || !backward) {
1405
+ this.drawLine(drawing, ctx);
1406
+ return;
1407
+ }
1408
+ ctx.beginPath();
1409
+ ctx.moveTo(backward.x, backward.y);
1410
+ ctx.lineTo(forward.x, forward.y);
1411
+ ctx.stroke();
1412
+ }
1413
+ static drawInfoLine(drawing, ctx) {
1414
+ this.drawLine(drawing, ctx);
1415
+ if (drawing.points.length < 2)
1416
+ return;
1417
+ const start = drawing.points[0];
1418
+ const end = drawing.points[1];
1419
+ if (!start || !end)
1420
+ return;
1421
+ const priceDelta = (end.price ?? 0) - (start.price ?? 0);
1422
+ const timeDelta = (end.time ?? 0) - (start.time ?? 0);
1423
+ const label = `ΔP ${priceDelta >= 0 ? '+' : ''}${priceDelta.toFixed(2)} | ΔT ${timeDelta >= 0 ? '+' : ''}${timeDelta.toFixed(0)}`;
1424
+ const midX = (start.x + end.x) / 2;
1425
+ const midY = (start.y + end.y) / 2;
1426
+ const fontSize = 10;
1427
+ const paddingX = 10;
1428
+ const paddingY = 6;
1429
+ ctx.font = `${fontSize}px Inter, sans-serif`;
1430
+ const textWidth = ctx.measureText(label).width;
1431
+ const boxWidth = textWidth + paddingX * 2;
1432
+ const boxHeight = fontSize + paddingY * 2;
1433
+ const boxX = midX - boxWidth / 2;
1434
+ const boxY = midY - boxHeight - 8;
1435
+ ctx.fillStyle = 'rgba(15,23,42,0.9)';
1436
+ ctx.strokeStyle = '#38bdf8';
1437
+ drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 10);
1438
+ ctx.fill();
1439
+ ctx.stroke();
1440
+ ctx.fillStyle = '#e2e8f0';
1441
+ ctx.textAlign = 'center';
1442
+ ctx.textBaseline = 'middle';
1443
+ ctx.fillText(label, midX, boxY + boxHeight / 2);
1444
+ }
1445
+ static drawTrendAngle(drawing, ctx) {
1446
+ this.drawLine(drawing, ctx);
1447
+ if (drawing.points.length < 2)
1448
+ return;
1449
+ const start = drawing.points[0];
1450
+ const end = drawing.points[1];
1451
+ if (!start || !end)
1452
+ return;
1453
+ const angleRad = Math.atan2(start.y - end.y, end.x - start.x);
1454
+ const angleDeg = ((angleRad * 180) / Math.PI + 360) % 360;
1455
+ const label = `${angleDeg.toFixed(1)}°`;
1456
+ ctx.font = '11px Inter, sans-serif';
1457
+ ctx.fillStyle = '#f8fafc';
1458
+ ctx.strokeStyle = '#2563eb';
1459
+ const paddingX = 8;
1460
+ const textWidth = ctx.measureText(label).width;
1461
+ const boxWidth = textWidth + paddingX * 2;
1462
+ const boxHeight = 18;
1463
+ const boxX = end.x + 10;
1464
+ const boxY = end.y - boxHeight - 4;
1465
+ ctx.beginPath();
1466
+ drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 8);
1467
+ ctx.fillStyle = 'rgba(2,6,23,0.85)';
1468
+ ctx.fill();
1469
+ ctx.stroke();
1470
+ ctx.fillStyle = '#f8fafc';
1471
+ ctx.textAlign = 'center';
1472
+ ctx.textBaseline = 'middle';
1473
+ ctx.fillText(label, boxX + boxWidth / 2, boxY + boxHeight / 2);
1474
+ }
1475
+ static drawHorizontalRay(drawing, ctx) {
1476
+ if (drawing.points.length < 1)
1477
+ return;
1478
+ const start = drawing.points[0];
1479
+ const guide = drawing.points[1] ?? start;
1480
+ if (!start || !guide)
1481
+ return;
1482
+ const direction = guide.x >= start.x ? 1 : -1;
1483
+ const targetX = direction > 0 ? ctx.canvas.width : 0;
1484
+ ctx.beginPath();
1485
+ ctx.moveTo(start.x, start.y);
1486
+ ctx.lineTo(targetX, start.y);
1487
+ ctx.stroke();
1488
+ }
1489
+ static drawInfiniteHorizontal(drawing, ctx) {
1490
+ if (drawing.points.length < 1)
1491
+ return;
1492
+ const start = drawing.points[0];
1493
+ if (!start)
1494
+ return;
1495
+ ctx.beginPath();
1496
+ ctx.moveTo(0, start.y);
1497
+ ctx.lineTo(ctx.canvas.width, start.y);
1498
+ ctx.stroke();
1499
+ }
1500
+ static drawVerticalLine(drawing, ctx) {
1501
+ if (drawing.points.length < 1)
1502
+ return;
1503
+ const start = drawing.points[0];
1504
+ if (!start)
1505
+ return;
1506
+ ctx.beginPath();
1507
+ ctx.moveTo(start.x, 0);
1508
+ ctx.lineTo(start.x, ctx.canvas.height);
1509
+ ctx.stroke();
1510
+ }
1511
+ static computeBoundaryIntersections(start, end, width, height) {
1512
+ const intersections = [];
1513
+ const dx = end.x - start.x;
1514
+ const dy = end.y - start.y;
1515
+ if (dx === 0 && dy === 0) {
1516
+ return intersections;
1517
+ }
1518
+ if (dx !== 0) {
1519
+ const tLeft = (0 - start.x) / dx;
1520
+ const yLeft = start.y + tLeft * dy;
1521
+ if (!Number.isNaN(yLeft) && yLeft >= 0 && yLeft <= height) {
1522
+ intersections.push({ x: 0, y: yLeft, t: tLeft });
1523
+ }
1524
+ const tRight = (width - start.x) / dx;
1525
+ const yRight = start.y + tRight * dy;
1526
+ if (!Number.isNaN(yRight) && yRight >= 0 && yRight <= height) {
1527
+ intersections.push({ x: width, y: yRight, t: tRight });
1528
+ }
1529
+ }
1530
+ if (dy !== 0) {
1531
+ const tTop = (0 - start.y) / dy;
1532
+ const xTop = start.x + tTop * dx;
1533
+ if (!Number.isNaN(xTop) && xTop >= 0 && xTop <= width) {
1534
+ intersections.push({ x: xTop, y: 0, t: tTop });
1535
+ }
1536
+ const tBottom = (height - start.y) / dy;
1537
+ const xBottom = start.x + tBottom * dx;
1538
+ if (!Number.isNaN(xBottom) && xBottom >= 0 && xBottom <= width) {
1539
+ intersections.push({ x: xBottom, y: height, t: tBottom });
1540
+ }
1541
+ }
1542
+ return intersections;
1543
+ }
1544
+ static drawRectangle(drawing, ctx) {
1545
+ if (drawing.points.length < 2)
1546
+ return;
1547
+ const start = drawing.points[0];
1548
+ const end = drawing.points[1];
1549
+ if (!start || !end)
1550
+ return;
1551
+ const rectX = Math.min(start.x, end.x);
1552
+ const rectY = Math.min(start.y, end.y);
1553
+ const rectWidth = Math.abs(end.x - start.x);
1554
+ const rectHeight = Math.abs(end.y - start.y);
1555
+ if (drawing.backgroundColor) {
1556
+ ctx.fillStyle = drawing.backgroundColor;
1557
+ ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
1558
+ }
1559
+ ctx.strokeRect(rectX, rectY, rectWidth, rectHeight);
1560
+ }
1561
+ static drawTriangle(drawing, ctx) {
1562
+ if (drawing.points.length < 3)
1563
+ return;
1564
+ const p1 = drawing.points[0];
1565
+ const p2 = drawing.points[1];
1566
+ const p3 = drawing.points[2];
1567
+ if (!p1 || !p2 || !p3)
1568
+ return;
1569
+ ctx.beginPath();
1570
+ ctx.moveTo(p1.x, p1.y);
1571
+ ctx.lineTo(p2.x, p2.y);
1572
+ ctx.lineTo(p3.x, p3.y);
1573
+ ctx.closePath();
1574
+ if (drawing.backgroundColor) {
1575
+ ctx.fillStyle = drawing.backgroundColor;
1576
+ ctx.fill();
1577
+ }
1578
+ ctx.stroke();
1579
+ }
1580
+ static drawChannel(drawing, ctx) {
1581
+ if (drawing.points.length < 4) {
1582
+ this.drawLine(drawing, ctx);
1583
+ return;
1584
+ }
1585
+ const [start1, end1, start2, end2] = drawing.points;
1586
+ if (!start1 || !end1 || !start2 || !end2)
1587
+ return;
1588
+ if (drawing.backgroundColor) {
1589
+ ctx.fillStyle = drawing.backgroundColor;
1590
+ ctx.beginPath();
1591
+ ctx.moveTo(start1.x, start1.y);
1592
+ ctx.lineTo(end1.x, end1.y);
1593
+ ctx.lineTo(end2.x, end2.y);
1594
+ ctx.lineTo(start2.x, start2.y);
1595
+ ctx.closePath();
1596
+ ctx.fill();
1597
+ }
1598
+ ctx.beginPath();
1599
+ ctx.moveTo(start1.x, start1.y);
1600
+ ctx.lineTo(end1.x, end1.y);
1601
+ ctx.stroke();
1602
+ ctx.beginPath();
1603
+ ctx.moveTo(start2.x, start2.y);
1604
+ ctx.lineTo(end2.x, end2.y);
1605
+ ctx.stroke();
1606
+ }
1607
+ static drawText(drawing, ctx) {
1608
+ if (drawing.points.length < 1 || !drawing.text)
1609
+ return;
1610
+ const point = drawing.points[0];
1611
+ if (!point)
1612
+ return;
1613
+ const fontSize = drawing.fontSize ?? 14;
1614
+ ctx.font = `${fontSize}px Inter, sans-serif`;
1615
+ const textMetrics = ctx.measureText(drawing.text);
1616
+ const textWidth = textMetrics.width;
1617
+ const paddingX = 12;
1618
+ const paddingY = 6;
1619
+ const boxWidth = textWidth + paddingX * 2;
1620
+ const boxHeight = fontSize + paddingY * 2;
1621
+ const originX = point.x - boxWidth / 2;
1622
+ const originY = point.y - boxHeight - 6;
1623
+ ctx.fillStyle = drawing.backgroundColor ?? 'rgba(15,23,42,0.85)';
1624
+ drawRoundedRect(ctx, originX, originY, boxWidth, boxHeight, 10);
1625
+ ctx.fill();
1626
+ ctx.fillStyle = drawing.color;
1627
+ ctx.textAlign = 'center';
1628
+ ctx.textBaseline = 'middle';
1629
+ ctx.fillText(drawing.text, point.x, originY + boxHeight / 2);
1630
+ }
1631
+ static drawIcon(drawing, ctx) {
1632
+ if (drawing.points.length < 1 || !drawing.icon)
1633
+ return;
1634
+ const point = drawing.points[0];
1635
+ if (!point)
1636
+ return;
1637
+ const fontSize = drawing.fontSize ?? 20;
1638
+ ctx.font = `${fontSize}px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Emoji", "Noto Color Emoji", "Android Emoji", "EmojiSymbols"`;
1639
+ ctx.textAlign = 'center';
1640
+ ctx.textBaseline = 'middle';
1641
+ ctx.fillStyle = drawing.color;
1642
+ ctx.fillText(drawing.icon, point.x, point.y);
1643
+ }
1644
+ static drawRuler(drawing, ctx) {
1645
+ this.drawLine(drawing, ctx);
1646
+ if (drawing.points.length < 2)
1647
+ return;
1648
+ const start = drawing.points[0];
1649
+ const end = drawing.points[1];
1650
+ const measurement = drawing.meta?.measurement;
1651
+ if (!start || !end || !measurement)
1652
+ return;
1653
+ const midX = (start.x + end.x) / 2;
1654
+ const midY = (start.y + end.y) / 2;
1655
+ const label = `${measurement.priceDelta >= 0 ? '+' : ''}${measurement.priceDelta.toFixed(2)} | ${measurement.percentDelta.toFixed(2)}%`;
1656
+ ctx.font = '11px Inter, sans-serif';
1657
+ const textWidth = ctx.measureText(label).width;
1658
+ const boxWidth = textWidth + 16;
1659
+ const boxHeight = 20;
1660
+ const boxX = midX - boxWidth / 2;
1661
+ const boxY = midY - boxHeight - 8;
1662
+ ctx.fillStyle = 'rgba(15,23,42,0.9)';
1663
+ ctx.strokeStyle = drawing.color;
1664
+ drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 8);
1665
+ ctx.fill();
1666
+ ctx.stroke();
1667
+ ctx.fillStyle = '#e2e8f0';
1668
+ ctx.textAlign = 'center';
1669
+ ctx.textBaseline = 'middle';
1670
+ ctx.fillText(label, midX, boxY + boxHeight / 2);
1671
+ }
1672
+ static drawFreehand(drawing, ctx) {
1673
+ if (drawing.points.length < 2)
1674
+ return;
1675
+ const firstPoint = drawing.points[0];
1676
+ if (!firstPoint)
1677
+ return;
1678
+ ctx.beginPath();
1679
+ ctx.moveTo(firstPoint.x, firstPoint.y);
1680
+ for (let i = 1; i < drawing.points.length; i++) {
1681
+ const point = drawing.points[i];
1682
+ if (point) {
1683
+ ctx.lineTo(point.x, point.y);
1684
+ }
1685
+ }
1686
+ ctx.stroke();
1687
+ }
1688
+ static drawFibonacci(drawing, ctx) {
1689
+ if (drawing.points.length < 2)
1690
+ return;
1691
+ const start = drawing.points[0];
1692
+ const end = drawing.points[1];
1693
+ if (!start || !end)
1694
+ return;
1695
+ const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
1696
+ const height = end.y - start.y;
1697
+ levels.forEach(level => {
1698
+ const y = start.y + height * level;
1699
+ ctx.beginPath();
1700
+ ctx.moveTo(start.x, y);
1701
+ ctx.lineTo(end.x, y);
1702
+ ctx.stroke();
1703
+ });
1704
+ }
1705
+ static drawCrossLine(drawing, ctx) {
1706
+ if (drawing.points.length < 1)
1707
+ return;
1708
+ const center = drawing.points[0];
1709
+ if (!center)
1710
+ return;
1711
+ // Draw horizontal line
1712
+ ctx.beginPath();
1713
+ ctx.moveTo(0, center.y);
1714
+ ctx.lineTo(ctx.canvas.width, center.y);
1715
+ ctx.stroke();
1716
+ // Draw vertical line
1717
+ ctx.beginPath();
1718
+ ctx.moveTo(center.x, 0);
1719
+ ctx.lineTo(center.x, ctx.canvas.height);
1720
+ ctx.stroke();
1721
+ }
1722
+ }
1723
+
1724
+ const UI_TEXT = {
1725
+ en: {
1726
+ symbolLabel: 'Symbol',
1727
+ quickTipsTitle: 'Quick start',
1728
+ quickTipsButton: 'Got it',
1729
+ quickTips: [
1730
+ 'Drag anywhere on the canvas to pan the chart.',
1731
+ 'Use your mouse wheel or trackpad to zoom smoothly.',
1732
+ 'Pick any tool on the left rail and draw on the canvas.'
1733
+ ],
1734
+ timeframeTitle: 'Quick timeframes',
1735
+ axis: {
1736
+ price: 'Price',
1737
+ time: 'Time'
1738
+ },
1739
+ buttons: {
1740
+ series: 'Change series',
1741
+ indicators: 'Indicators',
1742
+ config: 'Settings',
1743
+ fullscreenEnter: 'Enter fullscreen',
1744
+ fullscreenExit: 'Exit fullscreen',
1745
+ screenshot: 'Save screenshot',
1746
+ help: 'Quick tips'
1747
+ },
1748
+ ohlcLabels: {
1749
+ open: 'Open',
1750
+ high: 'High',
1751
+ low: 'Low',
1752
+ close: 'Close',
1753
+ volume: 'Volume'
1754
+ },
1755
+ priceChangeLabel: 'Change',
1756
+ seriesMenuTitle: 'Series type',
1757
+ indicatorsPanelTitle: 'Indicators',
1758
+ config: {
1759
+ title: 'Settings',
1760
+ language: 'Language',
1761
+ colors: 'Color theme',
1762
+ colorOptions: {
1763
+ green: 'Green / Red',
1764
+ red: 'Red / Green'
1765
+ },
1766
+ soon: 'More features coming soon...'
1767
+ },
1768
+ actions: {
1769
+ zoomIn: 'Zoom in',
1770
+ zoomOut: 'Zoom out',
1771
+ lock: 'Lock interactions',
1772
+ unlock: 'Unlock canvas',
1773
+ hide: 'Hide drawings',
1774
+ show: 'Show drawings',
1775
+ reset: 'Reset view',
1776
+ magnetOn: 'Magnet on',
1777
+ magnetOff: 'Magnet off',
1778
+ capture: 'Screenshot',
1779
+ indicators: 'Indicators'
1780
+ },
1781
+ toasts: {
1782
+ screenshot: 'Screenshot saved'
1783
+ },
1784
+ novice: {
1785
+ title: 'Novice mode',
1786
+ magnetOn: 'Magnet enabled for snapping.',
1787
+ magnetOff: 'Enable the magnet for precise snapping.',
1788
+ lockOn: 'Unlock the canvas to interact.',
1789
+ lockOff: 'You can lock the canvas from Actions.',
1790
+ hidden: 'Your drawings are hidden.',
1791
+ visible: 'Use “Hide” if you need a clean canvas.',
1792
+ reopen: 'Novice mode',
1793
+ checklistTitle: 'Starter checklist',
1794
+ legendTitle: 'Action legend',
1795
+ legendHint: 'Try every icon and see what it changes.',
1796
+ steps: {
1797
+ pan: {
1798
+ title: 'Move the chart',
1799
+ helper: 'Click and drag anywhere until the crosshair follows you.'
1800
+ },
1801
+ zoom: {
1802
+ title: 'Zoom smoothly',
1803
+ helper: 'Use the wheel or the zoom buttons to focus a zone.'
1804
+ },
1805
+ draw: {
1806
+ title: 'Place a drawing',
1807
+ helper: 'Pick any tool and drop at least one point on the canvas.'
1808
+ },
1809
+ capture: {
1810
+ title: 'Save a capture',
1811
+ helper: 'Use the camera icon to export a PNG.'
1812
+ }
1813
+ },
1814
+ labels: {
1815
+ magnet: 'Magnet',
1816
+ lock: 'Lock',
1817
+ drawings: 'Drawings'
1818
+ }
1819
+ }
1820
+ },
1821
+ es: {
1822
+ symbolLabel: 'Símbolo',
1823
+ quickTipsTitle: 'Guía rápida',
1824
+ quickTipsButton: 'Entendido',
1825
+ quickTips: [
1826
+ 'Haz clic y arrastra para desplazar el gráfico.',
1827
+ 'Usa la rueda o el trackpad para hacer zoom suave.',
1828
+ 'Selecciona una herramienta y dibuja sobre el lienzo.'
1829
+ ],
1830
+ timeframeTitle: 'Frecuencias rápidas',
1831
+ axis: {
1832
+ price: 'Precio',
1833
+ time: 'Tiempo'
1834
+ },
1835
+ buttons: {
1836
+ series: 'Cambiar serie',
1837
+ indicators: 'Indicadores',
1838
+ config: 'Configuración',
1839
+ fullscreenEnter: 'Pantalla completa',
1840
+ fullscreenExit: 'Salir de pantalla completa',
1841
+ screenshot: 'Guardar captura',
1842
+ help: 'Tips rápidos'
1843
+ },
1844
+ ohlcLabels: {
1845
+ open: 'Apertura',
1846
+ high: 'Máximo',
1847
+ low: 'Mínimo',
1848
+ close: 'Cierre',
1849
+ volume: 'Volumen'
1850
+ },
1851
+ priceChangeLabel: 'Cambio',
1852
+ seriesMenuTitle: 'Tipos de series',
1853
+ indicatorsPanelTitle: 'Indicadores',
1854
+ config: {
1855
+ title: 'Configuración',
1856
+ language: 'Idioma',
1857
+ colors: 'Tema de color',
1858
+ colorOptions: {
1859
+ green: 'Verde / Rojo',
1860
+ red: 'Rojo / Verde'
1861
+ },
1862
+ soon: 'Más funciones próximamente...'
1863
+ },
1864
+ actions: {
1865
+ zoomIn: 'Acercar',
1866
+ zoomOut: 'Alejar',
1867
+ lock: 'Bloquear interacciones',
1868
+ unlock: 'Desbloquear',
1869
+ hide: 'Ocultar dibujos',
1870
+ show: 'Mostrar dibujos',
1871
+ reset: 'Restablecer vista',
1872
+ magnetOn: 'Imán activado',
1873
+ magnetOff: 'Imán desactivado',
1874
+ capture: 'Captura',
1875
+ indicators: 'Indicadores'
1876
+ },
1877
+ toasts: {
1878
+ screenshot: 'Captura descargada'
1879
+ },
1880
+ novice: {
1881
+ title: 'Modo novato',
1882
+ magnetOn: 'Imán activo para ajustar precios.',
1883
+ magnetOff: 'Activa el imán para más precisión.',
1884
+ lockOn: 'Desbloquea el lienzo para interactuar.',
1885
+ lockOff: 'Puedes bloquear el lienzo en Acciones.',
1886
+ hidden: 'Tus dibujos están ocultos.',
1887
+ visible: 'Usa “Ocultar” si quieres un lienzo limpio.',
1888
+ reopen: 'Modo novato',
1889
+ checklistTitle: 'Checklist inicial',
1890
+ legendTitle: 'Leyenda de acciones',
1891
+ legendHint: 'Prueba cada icono y observa el cambio.',
1892
+ steps: {
1893
+ pan: {
1894
+ title: 'Mueve el gráfico',
1895
+ helper: 'Haz clic y arrastra hasta que el cursor te siga.'
1896
+ },
1897
+ zoom: {
1898
+ title: 'Zoom suave',
1899
+ helper: 'Usa la rueda o los botones de zoom para enfocar una zona.'
1900
+ },
1901
+ draw: {
1902
+ title: 'Dibuja algo',
1903
+ helper: 'Elige una herramienta y deja al menos un punto en el lienzo.'
1904
+ },
1905
+ capture: {
1906
+ title: 'Guarda una captura',
1907
+ helper: 'Toca la cámara para exportar un PNG.'
1908
+ }
1909
+ },
1910
+ labels: {
1911
+ magnet: 'Imán',
1912
+ lock: 'Bloqueo',
1913
+ drawings: 'Dibujos'
1914
+ }
1915
+ }
1916
+ }
1917
+ };
1918
+ const TOOL_COLOR_MAP = {
1919
+ trendline: '#8ab4ff',
1920
+ ray: '#8ab4ff',
1921
+ 'info-line': '#a5b4fc',
1922
+ 'extended-line': '#93c5fd',
1923
+ 'trend-angle': '#7dd3fc',
1924
+ horizontal: '#fbbf24',
1925
+ 'horizontal-line': '#fbbf24',
1926
+ 'horizontal-ray': '#facc15',
1927
+ 'vertical-line': '#fdba74',
1928
+ 'cross-line': '#fda4af',
1929
+ parallel: '#a78bfa',
1930
+ rectangle: '#60a5fa',
1931
+ triangle: '#f472b6',
1932
+ channel: '#34d399',
1933
+ 'regression-trend': '#4ade80',
1934
+ 'flat-top-bottom': '#2dd4bf',
1935
+ 'disjoint-channel': '#22d3ee',
1936
+ pitchfork: '#f472b6',
1937
+ 'schiff-pitchfork': '#f472b6',
1938
+ 'modified-schiff-pitchfork': '#fb7185',
1939
+ 'inside-pitchfork': '#fb7185',
1940
+ fibonacci: '#c4b5fd',
1941
+ text: '#e5e7eb',
1942
+ icon: '#fcd34d',
1943
+ ruler: '#f87171',
1944
+ freehand: '#c084fc'
1945
+ };
1946
+ const metricCardStyle = {
1947
+ background: 'rgba(15,23,42,0.85)',
1948
+ border: '1px solid #111827',
1949
+ borderRadius: '18px',
1950
+ padding: '14px 16px',
1951
+ display: 'flex',
1952
+ flexDirection: 'column',
1953
+ gap: '6px',
1954
+ minHeight: '90px'
1955
+ };
1956
+ const metricLabelStyle = {
1957
+ fontSize: '10px',
1958
+ textTransform: 'uppercase',
1959
+ letterSpacing: '0.08em',
1960
+ color: '#94a3b8'
1961
+ };
1962
+ const metricValueStyle = {
1963
+ fontSize: '18px',
1964
+ fontWeight: 700,
1965
+ color: '#f8fafc'
1966
+ };
1967
+ const metricSubValueStyle = {
1968
+ fontSize: '12px',
1969
+ color: '#8b9cc2'
1970
+ };
1971
+ const LINE_TOOL_TYPES = [
1972
+ 'trendline',
1973
+ 'ray',
1974
+ 'info-line',
1975
+ 'extended-line',
1976
+ 'trend-angle',
1977
+ 'horizontal',
1978
+ 'horizontal-line',
1979
+ 'horizontal-ray',
1980
+ 'vertical-line',
1981
+ 'cross-line',
1982
+ 'parallel',
1983
+ 'channel',
1984
+ 'regression-trend',
1985
+ 'flat-top-bottom',
1986
+ 'disjoint-channel',
1987
+ 'pitchfork',
1988
+ 'schiff-pitchfork',
1989
+ 'modified-schiff-pitchfork',
1990
+ 'inside-pitchfork'
1991
+ ];
1992
+ const LINE_TOOL_SET = new Set(LINE_TOOL_TYPES);
1993
+ const THEME_PRESETS = {
1994
+ dark: {
1995
+ pageBg: '#050910',
1996
+ heroFrom: '#111827',
1997
+ heroTo: '#0b1120',
1998
+ panelBg: 'rgba(15,23,42,0.9)',
1999
+ panelBorder: '#1f2937',
2000
+ cardBg: 'rgba(15,23,42,0.85)',
2001
+ cardBorder: '#111827',
2002
+ textPrimary: '#f8fafc',
2003
+ textSecondary: '#94a3b8',
2004
+ accent: '#2563eb',
2005
+ accentSoft: 'rgba(37,99,235,0.2)',
2006
+ railBg: 'rgba(2,6,23,0.85)',
2007
+ railBorder: '#111827',
2008
+ plotBg: '#020617',
2009
+ plotBorder: '#111827',
2010
+ scaleBg: 'rgba(15,23,42,0.9)',
2011
+ overlayBg: 'rgba(5,7,15,0.85)'
2012
+ },
2013
+ blue: {
2014
+ pageBg: '#060e1f',
2015
+ heroFrom: '#0f172a',
2016
+ heroTo: '#1d4ed8',
2017
+ panelBg: 'rgba(6,11,25,0.9)',
2018
+ panelBorder: '#1e3a8a',
2019
+ cardBg: 'rgba(13,23,42,0.92)',
2020
+ cardBorder: '#1d4ed8',
2021
+ textPrimary: '#e2e8f0',
2022
+ textSecondary: '#a5b4fc',
2023
+ accent: '#38bdf8',
2024
+ accentSoft: 'rgba(56,189,248,0.2)',
2025
+ railBg: 'rgba(4,8,20,0.85)',
2026
+ railBorder: '#1e3a8a',
2027
+ plotBg: '#020b1f',
2028
+ plotBorder: '#1d4ed8',
2029
+ scaleBg: 'rgba(6,11,25,0.95)',
2030
+ overlayBg: 'rgba(4,8,20,0.8)'
2031
+ },
2032
+ light: {
2033
+ pageBg: '#f8fafc',
2034
+ heroFrom: '#e3e8ff',
2035
+ heroTo: '#fdfdff',
2036
+ panelBg: '#ffffff',
2037
+ panelBorder: '#d7def1',
2038
+ cardBg: '#ffffff',
2039
+ cardBorder: '#dde3f7',
2040
+ textPrimary: '#0f172a',
2041
+ textSecondary: '#5b6473',
2042
+ accent: '#2563eb',
2043
+ accentSoft: 'rgba(37,99,235,0.18)',
2044
+ railBg: '#ffffff',
2045
+ railBorder: '#dbe4fb',
2046
+ plotBg: '#ffffff',
2047
+ plotBorder: '#dbe4fb',
2048
+ scaleBg: '#f3f6ff',
2049
+ overlayBg: 'rgba(15,23,42,0.18)'
2050
+ }
2051
+ };
2052
+ const DEFAULT_CUSTOM_THEME = { ...THEME_PRESETS.dark };
2053
+ const CUSTOM_THEME_FIELDS = [
2054
+ { key: 'pageBg', label: 'Page background' },
2055
+ { key: 'heroFrom', label: 'Header gradient (from)' },
2056
+ { key: 'heroTo', label: 'Header gradient (to)' },
2057
+ { key: 'accent', label: 'Accent color' },
2058
+ { key: 'plotBg', label: 'Plot background' }
2059
+ ];
2060
+ const MIN_CANDLE_PX = 4;
2061
+ const MAX_CANDLE_PX = 28;
2062
+ const BUFFER_FRACTION = 0.18;
2063
+ const INERTIA_DURATION_MS = 900;
2064
+ const ZOOM_MIN = 0.2;
2065
+ const ZOOM_MAX = 6;
2066
+ const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
2067
+ const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
2068
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
2069
+ const alignStroke = (value) => Math.round(value) + 0.5;
2070
+ const createParallelPoints = (start, end, offset) => {
2071
+ const dx = end.x - start.x;
2072
+ const dy = end.y - start.y;
2073
+ const length = Math.hypot(dx, dy) || 1;
2074
+ const offsetX = (-dy / length) * offset;
2075
+ const offsetY = (dx / length) * offset;
2076
+ const start2 = { ...start, x: start.x + offsetX, y: start.y + offsetY };
2077
+ const end2 = { ...end, x: end.x + offsetX, y: end.y + offsetY };
2078
+ return [start, end, start2, end2];
2079
+ };
2080
+ const getChannelOffset = (tool) => {
2081
+ switch (tool) {
2082
+ case 'channel':
2083
+ return 40;
2084
+ case 'regression-trend':
2085
+ return 36;
2086
+ case 'flat-top-bottom':
2087
+ return 28;
2088
+ case 'disjoint-channel':
2089
+ return 48;
2090
+ case 'pitchfork':
2091
+ case 'schiff-pitchfork':
2092
+ case 'modified-schiff-pitchfork':
2093
+ case 'inside-pitchfork':
2094
+ return 34;
2095
+ default:
2096
+ return 28;
2097
+ }
2098
+ };
2099
+ const createTrianglePoints = (start, end) => {
2100
+ const apexDirection = start.y < end.y ? -1 : 1;
2101
+ const apexHeight = Math.max(24, Math.abs(end.y - start.y));
2102
+ const apex = {
2103
+ ...start,
2104
+ x: (start.x + end.x) / 2,
2105
+ y: start.y + apexDirection * apexHeight,
2106
+ time: (start.time + end.time) / 2
2107
+ };
2108
+ return [start, end, apex];
2109
+ };
2110
+ const buildRulerMeta = (start, end) => ({
2111
+ distancePx: Math.hypot(end.x - start.x, end.y - start.y),
2112
+ priceDelta: end.price - start.price,
2113
+ percentDelta: start.price !== 0 ? ((end.price - start.price) / start.price) * 100 : 0
2114
+ });
2115
+ const computeViewportCount = (chartWidth, timeframe, zoomLevel, getVisibleCountFn) => {
2116
+ if (chartWidth <= 0)
2117
+ return 0;
2118
+ const timeframeHint = getVisibleCountFn(timeframe);
2119
+ const pxPerCandle = clamp(12 / Math.max(zoomLevel, 0.1), MIN_CANDLE_PX, MAX_CANDLE_PX);
2120
+ const widthBased = Math.max(10, Math.floor(chartWidth / pxPerCandle));
2121
+ const blended = (timeframeHint + widthBased) / 2;
2122
+ return Math.max(10, Math.round(blended / Math.max(zoomLevel, 0.1)));
2123
+ };
2124
+ const computeVisibleWindow = (dataset, chartWidth, timeframe, zoomLevel, panOffset, getVisibleCountFn) => {
2125
+ const datasetLength = dataset.length;
2126
+ if (!datasetLength || chartWidth <= 0) {
2127
+ return {
2128
+ visibleSlice: [],
2129
+ meta: {
2130
+ start: 0,
2131
+ end: 0,
2132
+ bufferStart: 0,
2133
+ bufferEnd: 0,
2134
+ visibleCount: 0,
2135
+ buffer: 0
2136
+ }
2137
+ };
2138
+ }
2139
+ const visibleCount = Math.min(datasetLength, computeViewportCount(chartWidth, timeframe, zoomLevel, getVisibleCountFn));
2140
+ const buffer = Math.min(200, Math.max(10, Math.round(visibleCount * BUFFER_FRACTION)));
2141
+ const unclampedStart = datasetLength - visibleCount - panOffset;
2142
+ const start = clamp(Math.round(unclampedStart), 0, datasetLength);
2143
+ const end = Math.min(datasetLength, start + visibleCount);
2144
+ const bufferStart = clamp(start - buffer, 0, datasetLength);
2145
+ const bufferEnd = clamp(end + buffer, bufferStart, datasetLength);
2146
+ const visibleSlice = dataset.slice(start, end);
2147
+ return {
2148
+ visibleSlice,
2149
+ meta: {
2150
+ start,
2151
+ end,
2152
+ bufferStart,
2153
+ bufferEnd,
2154
+ visibleCount: visibleSlice.length,
2155
+ buffer
2156
+ }
2157
+ };
2158
+ };
2159
+ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange }) => {
2160
+ const chartRef = React.useRef(null);
2161
+ const volumeRef = React.useRef(null);
2162
+ const gridRef = React.useRef(null);
2163
+ const overlayRef = React.useRef(null);
2164
+ const drawingsRef = React.useRef(null);
2165
+ const mousePosRef = React.useRef({ x: 0, y: 0 });
2166
+ const showCrosshairRef = React.useRef(false);
2167
+ const containerRef = React.useRef(null);
2168
+ const plotAreaRef = React.useRef(null);
2169
+ const priceWindowRef = React.useRef({ min: 0, max: 1 });
2170
+ const [dimensions, setDimensions] = React.useState({ width: 800, height: 400, cssWidth: 800, cssHeight: 400 });
2171
+ const isDraggingRef = React.useRef(false);
2172
+ const lastMouseXRef = React.useRef(0);
2173
+ const coordsRef = React.useRef(createCoordinateSystem());
2174
+ const [calculatedIndicators, setCalculatedIndicators] = React.useState([]);
2175
+ const visibleWindowRef = React.useRef({ start: 0, end: 0, bufferStart: 0, bufferEnd: 0, visibleCount: 0, buffer: 0 });
2176
+ const visibleDataRef = React.useRef([]);
2177
+ const panOffsetRef = React.useRef(0);
2178
+ const zoomLevelRef = React.useRef(1);
2179
+ const dragSampleRef = React.useRef({ velocity: 0, lastTs: 0 });
2180
+ const inertiaRef = React.useRef({
2181
+ rafId: null,
2182
+ startTime: 0,
2183
+ lastTime: 0,
2184
+ initialVelocity: 0
2185
+ });
2186
+ const zoomAnimationRef = React.useRef(null);
2187
+ const zoomAnimationStateRef = React.useRef(null);
2188
+ // Zustand store
2189
+ const { data: storeData, visibleData, timeframe, zoomLevel, panOffset, indicators, showIndicatorsPanel, drawings, magnetEnabled, currentPrice, priceChange, priceChangePercent, activeTool, cursorType, strokeColor, strokeWidth, isDrawing, currentDrawing, selectedEmoji, interactionsLocked, drawingsHidden, toggleInteractionsLock, toggleDrawingsHidden, setData, setVisibleData, setTimeframe, setZoomLevel, setPanOffset, addIndicator, removeIndicator, toggleIndicatorsPanel, toggleMagnet, setIsDrawing, setCurrentDrawing, addDrawing, setSelectedEmoji, deleteDrawing, setCursorType } = useChartStore();
2190
+ const handleTimeframeChange = React.useCallback((nextTimeframe) => {
2191
+ setTimeframe(nextTimeframe);
2192
+ if (onTimeframeChange) {
2193
+ onTimeframeChange(nextTimeframe);
2194
+ }
2195
+ }, [setTimeframe, onTimeframeChange]);
2196
+ const getVisibleCount = React.useCallback((tf) => {
2197
+ switch (tf) {
2198
+ case '1m': return 200;
2199
+ case '3m': return 134;
2200
+ case '5m': return 100;
2201
+ case '15m': return 50;
2202
+ case '30m': return 25;
2203
+ case '1h': return 50;
2204
+ case '4h': return 25;
2205
+ case '12h': return 15;
2206
+ case '1D': return 10;
2207
+ case '3D': return 8;
2208
+ case '1W': return 5;
2209
+ case '1M': return 3;
2210
+ default: return 25;
2211
+ }
2212
+ }, []);
2213
+ React.useEffect(() => {
2214
+ setData(data);
2215
+ }, [data, setData]);
2216
+ React.useEffect(() => {
2217
+ const newIndicators = [];
2218
+ indicators.forEach(config => {
2219
+ if (!config.visible)
2220
+ return;
2221
+ try {
2222
+ switch (config.name) {
2223
+ case 'SMA':
2224
+ newIndicators.push(IndicatorCalculator.calculateSMA(storeData, config.params.period));
2225
+ break;
2226
+ case 'EMA':
2227
+ newIndicators.push(IndicatorCalculator.calculateEMA(storeData, config.params.period));
2228
+ break;
2229
+ case 'RSI':
2230
+ newIndicators.push(IndicatorCalculator.calculateRSI(storeData, config.params.period));
2231
+ break;
2232
+ case 'MACD':
2233
+ const macdResult = IndicatorCalculator.calculateMACD(storeData, config.params.fastPeriod, config.params.slowPeriod, config.params.signalPeriod);
2234
+ newIndicators.push(macdResult.macd, macdResult.signal, macdResult.histogram);
2235
+ break;
2236
+ }
2237
+ }
2238
+ catch (error) {
2239
+ console.error(`Error calculating ${config.name}:`, error);
2240
+ }
2241
+ });
2242
+ setCalculatedIndicators(newIndicators);
2243
+ }, [storeData, indicators]);
2244
+ React.useEffect(() => {
2245
+ const updateDimensions = () => {
2246
+ if (plotAreaRef.current) {
2247
+ const rect = plotAreaRef.current.getBoundingClientRect();
2248
+ const dpr = window.devicePixelRatio || 1;
2249
+ setDimensions({ width: rect.width * dpr, height: rect.height * dpr, cssWidth: rect.width, cssHeight: rect.height });
2250
+ }
2251
+ };
2252
+ updateDimensions();
2253
+ window.addEventListener('resize', updateDimensions);
2254
+ return () => window.removeEventListener('resize', updateDimensions);
2255
+ }, []);
2256
+ const { width, height, cssWidth, cssHeight } = dimensions;
2257
+ const priceScaleWidth = 60;
2258
+ const timeScaleHeight = 48;
2259
+ const volumeHeight = 120;
2260
+ const chartWidth = Math.max(1, cssWidth - priceScaleWidth);
2261
+ const chartHeight = Math.max(1, cssHeight - timeScaleHeight - volumeHeight);
2262
+ const overlayHeight = Math.max(1, cssHeight - timeScaleHeight);
2263
+ React.useEffect(() => {
2264
+ const { visibleSlice, meta } = computeVisibleWindow(storeData, chartWidth, timeframe, zoomLevel, panOffset, getVisibleCount);
2265
+ setVisibleData(visibleSlice);
2266
+ visibleWindowRef.current = meta;
2267
+ }, [storeData, chartWidth, timeframe, zoomLevel, panOffset, setVisibleData]);
2268
+ React.useEffect(() => {
2269
+ visibleDataRef.current = visibleData;
2270
+ }, [visibleData]);
2271
+ React.useEffect(() => {
2272
+ panOffsetRef.current = panOffset;
2273
+ }, [panOffset]);
2274
+ React.useEffect(() => {
2275
+ zoomLevelRef.current = zoomLevel;
2276
+ }, [zoomLevel]);
2277
+ const getVisibleCandles = React.useCallback(() => visibleDataRef.current, []);
2278
+ React.useCallback(() => visibleWindowRef.current, []);
2279
+ const clampPanOffset = React.useCallback((value) => clamp(value, -storeData.length, storeData.length), [storeData.length]);
2280
+ const setPanOffsetSafe = React.useCallback((value) => {
2281
+ const next = clampPanOffset(value);
2282
+ panOffsetRef.current = next;
2283
+ setPanOffset(next);
2284
+ }, [clampPanOffset, setPanOffset]);
2285
+ const clampZoomLevel = React.useCallback((value) => clamp(value, ZOOM_MIN, ZOOM_MAX), []);
2286
+ const setZoomLevelSafe = React.useCallback((value) => {
2287
+ const next = clampZoomLevel(value);
2288
+ zoomLevelRef.current = next;
2289
+ setZoomLevel(next);
2290
+ }, [clampZoomLevel, setZoomLevel]);
2291
+ const handleCustomThemeChange = React.useCallback((key, value) => {
2292
+ setCustomTheme((prev) => ({ ...prev, [key]: value }));
2293
+ }, []);
2294
+ const applyPanDelta = React.useCallback((deltaPx) => {
2295
+ if (!chartWidth)
2296
+ return;
2297
+ const candles = getVisibleCandles();
2298
+ const pxPerCandle = chartWidth / Math.max(candles.length || 1, 1);
2299
+ if (!isFinite(pxPerCandle) || pxPerCandle === 0)
2300
+ return;
2301
+ const deltaOffset = deltaPx / Math.max(pxPerCandle, 1e-3);
2302
+ setPanOffsetSafe(panOffsetRef.current + deltaOffset);
2303
+ }, [chartWidth, getVisibleCandles, setPanOffsetSafe]);
2304
+ const cancelInertia = React.useCallback(() => {
2305
+ if (inertiaRef.current.rafId !== null) {
2306
+ cancelAnimationFrame(inertiaRef.current.rafId);
2307
+ }
2308
+ inertiaRef.current = {
2309
+ rafId: null,
2310
+ startTime: 0,
2311
+ lastTime: 0,
2312
+ initialVelocity: 0
2313
+ };
2314
+ }, []);
2315
+ const startInertia = React.useCallback((initialVelocity) => {
2316
+ if (Math.abs(initialVelocity) < 0.002)
2317
+ return;
2318
+ cancelInertia();
2319
+ inertiaRef.current.initialVelocity = initialVelocity;
2320
+ inertiaRef.current.startTime = performance.now();
2321
+ inertiaRef.current.lastTime = inertiaRef.current.startTime;
2322
+ const step = (timestamp) => {
2323
+ const elapsed = timestamp - inertiaRef.current.startTime;
2324
+ const progress = Math.min(1, elapsed / INERTIA_DURATION_MS);
2325
+ const eased = 1 - easeOutQuad(progress);
2326
+ const currentVelocity = inertiaRef.current.initialVelocity * eased;
2327
+ const deltaTime = timestamp - inertiaRef.current.lastTime;
2328
+ inertiaRef.current.lastTime = timestamp;
2329
+ if (Math.abs(currentVelocity) < 0.001 || progress >= 1) {
2330
+ cancelInertia();
2331
+ return;
2332
+ }
2333
+ applyPanDelta(currentVelocity * deltaTime);
2334
+ inertiaRef.current.rafId = requestAnimationFrame(step);
2335
+ };
2336
+ inertiaRef.current.rafId = requestAnimationFrame(step);
2337
+ }, [applyPanDelta, cancelInertia]);
2338
+ const cancelZoomAnimation = React.useCallback(() => {
2339
+ if (zoomAnimationRef.current) {
2340
+ cancelAnimationFrame(zoomAnimationRef.current);
2341
+ zoomAnimationRef.current = null;
2342
+ }
2343
+ zoomAnimationStateRef.current = null;
2344
+ }, []);
2345
+ const applyZoomAtRatio = React.useCallback((nextZoom, anchorRatio) => {
2346
+ const datasetLength = storeData.length;
2347
+ if (!datasetLength) {
2348
+ setZoomLevelSafe(nextZoom);
2349
+ return;
2350
+ }
2351
+ const prevCount = computeViewportCount(chartWidth, timeframe, zoomLevelRef.current, getVisibleCount);
2352
+ const nextCount = computeViewportCount(chartWidth, timeframe, nextZoom, getVisibleCount);
2353
+ if (!prevCount || !nextCount) {
2354
+ setZoomLevelSafe(nextZoom);
2355
+ return;
2356
+ }
2357
+ const startBefore = clamp(datasetLength - prevCount - panOffsetRef.current, 0, Math.max(0, datasetLength - prevCount));
2358
+ const anchorIndex = startBefore + anchorRatio * prevCount;
2359
+ const nextStart = clamp(anchorIndex - anchorRatio * nextCount, 0, Math.max(0, datasetLength - nextCount));
2360
+ const nextPan = datasetLength - nextCount - nextStart;
2361
+ setZoomLevelSafe(nextZoom);
2362
+ setPanOffsetSafe(nextPan);
2363
+ }, [chartWidth, timeframe, storeData.length, setZoomLevelSafe, setPanOffsetSafe, getVisibleCount]);
2364
+ const startZoomAnimation = React.useCallback((targetZoom, anchorRatio) => {
2365
+ const clampedTarget = clampZoomLevel(targetZoom);
2366
+ const currentZoom = zoomLevelRef.current;
2367
+ if (Math.abs(clampedTarget - currentZoom) < 0.001) {
2368
+ applyZoomAtRatio(clampedTarget, anchorRatio);
2369
+ return;
2370
+ }
2371
+ cancelZoomAnimation();
2372
+ zoomAnimationStateRef.current = {
2373
+ from: currentZoom,
2374
+ to: clampedTarget,
2375
+ startTime: performance.now(),
2376
+ duration: 220,
2377
+ anchorRatio
2378
+ };
2379
+ const animate = (timestamp) => {
2380
+ const state = zoomAnimationStateRef.current;
2381
+ if (!state)
2382
+ return;
2383
+ const progress = Math.min(1, (timestamp - state.startTime) / state.duration);
2384
+ const eased = easeInOutCubic(progress);
2385
+ const nextZoom = state.from + (state.to - state.from) * eased;
2386
+ applyZoomAtRatio(nextZoom, state.anchorRatio);
2387
+ if (progress < 1) {
2388
+ zoomAnimationRef.current = requestAnimationFrame(animate);
2389
+ }
2390
+ else {
2391
+ zoomAnimationRef.current = null;
2392
+ zoomAnimationStateRef.current = null;
2393
+ }
2394
+ };
2395
+ zoomAnimationRef.current = requestAnimationFrame(animate);
2396
+ }, [applyZoomAtRatio, cancelZoomAnimation, clampZoomLevel]);
2397
+ React.useEffect(() => {
2398
+ return () => {
2399
+ cancelInertia();
2400
+ cancelZoomAnimation();
2401
+ };
2402
+ }, [cancelInertia, cancelZoomAnimation]);
2403
+ // Draw drawings
2404
+ React.useEffect(() => {
2405
+ const drawingsCtx = drawingsRef.current?.getContext('2d');
2406
+ if (drawingsCtx) {
2407
+ drawingsCtx.clearRect(0, 0, chartWidth, overlayHeight);
2408
+ if (drawingsHidden) {
2409
+ return;
2410
+ }
2411
+ drawings.forEach(drawing => {
2412
+ DrawingRenderer.draw(drawing, drawingsCtx);
2413
+ });
2414
+ if (currentDrawing) {
2415
+ DrawingRenderer.draw(currentDrawing, drawingsCtx);
2416
+ }
2417
+ }
2418
+ }, [drawings, currentDrawing, chartWidth, overlayHeight, drawingsHidden]);
2419
+ const [priceLabels, setPriceLabels] = React.useState([]);
2420
+ const [timeLabels, setTimeLabels] = React.useState([]);
2421
+ const [clickedPrice, setClickedPrice] = React.useState(null);
2422
+ const [showConfigPanel, setShowConfigPanel] = React.useState(false);
2423
+ const [language, setLanguage] = React.useState('en');
2424
+ const [colorScheme, setColorScheme] = React.useState('green');
2425
+ const [themePreset, setThemePreset] = React.useState('dark');
2426
+ const [customTheme, setCustomTheme] = React.useState(DEFAULT_CUSTOM_THEME);
2427
+ const resolvedPreset = themePreset === 'custom' ? 'dark' : themePreset;
2428
+ const activeTheme = themePreset === 'custom' ? customTheme : THEME_PRESETS[resolvedPreset];
2429
+ const themeOptions = [
2430
+ { key: 'dark', label: 'Dark' },
2431
+ { key: 'blue', label: 'Blue' },
2432
+ { key: 'light', label: 'Light' },
2433
+ { key: 'custom', label: 'Custom' }
2434
+ ];
2435
+ const elevatedShadow = themePreset === 'light' ? '0 15px 30px rgba(15,23,42,0.12)' : '0 15px 40px rgba(0,0,0,0.35)';
2436
+ const surfaceShadow = themePreset === 'light' ? '0 30px 50px rgba(15,23,42,0.12)' : '0 30px 60px rgba(0,0,0,0.35)';
2437
+ const iconBaseBg = themePreset === 'light' ? '#0f172a' : 'rgba(15,23,42,0.6)';
2438
+ const iconBaseColor = '#f8fafc';
2439
+ const leftRailBaseBg = themePreset === 'light' ? '#0f172a' : activeTheme.panelBg;
2440
+ const strings = React.useMemo(() => UI_TEXT[language] ?? UI_TEXT.en, [language]);
2441
+ const [isFullscreen, setIsFullscreen] = React.useState(false);
2442
+ const [selectedCandleIndex, setSelectedCandleIndex] = React.useState(null);
2443
+ const [seriesType, setSeriesType] = React.useState('candles');
2444
+ const [showSeriesMenu, setShowSeriesMenu] = React.useState(false);
2445
+ const [isSeriesLoading, setIsSeriesLoading] = React.useState(false);
2446
+ const [showQuickTips, setShowQuickTips] = React.useState(true);
2447
+ const [toastMessage, setToastMessage] = React.useState(null);
2448
+ const [showNoteModal, setShowNoteModal] = React.useState(false);
2449
+ const [pendingNotePoint, setPendingNotePoint] = React.useState(null);
2450
+ const [noteDraft, setNoteDraft] = React.useState('');
2451
+ const [isMobile, setIsMobile] = React.useState(false);
2452
+ React.useEffect(() => {
2453
+ if (!plotAreaRef.current)
2454
+ return;
2455
+ const rect = plotAreaRef.current.getBoundingClientRect();
2456
+ const dpr = window.devicePixelRatio || 1;
2457
+ setDimensions({ width: rect.width * dpr, height: rect.height * dpr, cssWidth: rect.width, cssHeight: rect.height });
2458
+ }, [isFullscreen, showIndicatorsPanel, showConfigPanel, showSeriesMenu]);
2459
+ React.useEffect(() => {
2460
+ const checkMobile = () => setIsMobile(window.innerWidth < 768);
2461
+ checkMobile();
2462
+ window.addEventListener('resize', checkMobile);
2463
+ return () => window.removeEventListener('resize', checkMobile);
2464
+ }, []);
2465
+ React.useEffect(() => {
2466
+ if (!visibleData.length)
2467
+ return;
2468
+ setIsSeriesLoading(true);
2469
+ const id = window.setTimeout(() => setIsSeriesLoading(false), 250);
2470
+ return () => window.clearTimeout(id);
2471
+ }, [seriesType, visibleData.length]);
2472
+ const locale = language === 'es' ? 'es-ES' : 'en-US';
2473
+ const numberFormatter = React.useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }), [locale]);
2474
+ const shortNumberFormatter = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
2475
+ const timeFormatter = React.useMemo(() => new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }), [locale]);
2476
+ const latencyMs = React.useMemo(() => Math.round(20 + Math.random() * 15), []);
2477
+ const derivedStats = React.useMemo(() => {
2478
+ if (!storeData.length) {
2479
+ return null;
2480
+ }
2481
+ const highs = storeData.map((d) => d.high);
2482
+ const lows = storeData.map((d) => d.low);
2483
+ const closes = storeData.map((d) => d.close);
2484
+ const volumes = storeData.map((d) => d.volume ?? 0);
2485
+ const maxHigh = Math.max(...highs);
2486
+ const minLow = Math.min(...lows);
2487
+ const range = maxHigh - minLow;
2488
+ const lastClose = closes[closes.length - 1] ?? 0;
2489
+ const rangePct = lastClose ? (range / lastClose) * 100 : 0;
2490
+ const avgVolume = volumes.reduce((acc, value) => acc + value, 0) / Math.max(1, volumes.length);
2491
+ let volatility = 0;
2492
+ if (closes.length > 1) {
2493
+ let diffSum = 0;
2494
+ for (let i = 1; i < closes.length; i++) {
2495
+ const current = closes[i] ?? closes[i - 1] ?? 0;
2496
+ const prevValue = closes[i - 1] ?? current;
2497
+ if (prevValue !== 0) {
2498
+ diffSum += Math.abs(current - prevValue) / prevValue;
2499
+ }
2500
+ }
2501
+ volatility = (diffSum / (closes.length - 1)) * 100;
2502
+ }
2503
+ const hour = new Date().getUTCHours();
2504
+ const session = hour < 7 ? 'Asia' : hour < 14 ? 'Europe' : hour < 21 ? 'New York' : 'Pacific';
2505
+ return {
2506
+ range,
2507
+ rangePct,
2508
+ avgVolume,
2509
+ volatility,
2510
+ session,
2511
+ samples: storeData.length
2512
+ };
2513
+ }, [storeData]);
2514
+ const showToast = React.useCallback((message) => {
2515
+ setToastMessage(message);
2516
+ }, []);
2517
+ React.useEffect(() => {
2518
+ if (!toastMessage)
2519
+ return;
2520
+ const id = window.setTimeout(() => setToastMessage(null), 2400);
2521
+ return () => window.clearTimeout(id);
2522
+ }, [toastMessage]);
2523
+ React.useEffect(() => {
2524
+ const candles = getVisibleCandles();
2525
+ if (!candles.length)
2526
+ return;
2527
+ const bounds = getDataBounds(candles);
2528
+ const rawRange = bounds.maxPrice - bounds.minPrice;
2529
+ const padding = rawRange === 0 ? Math.max(1, bounds.maxPrice * 0.01 || 1) : rawRange * 0.05;
2530
+ const minPrice = Math.max(0, bounds.minPrice - padding);
2531
+ const maxPrice = bounds.maxPrice + padding;
2532
+ const priceRange = Math.max(1e-6, maxPrice - minPrice);
2533
+ const maxVolume = Math.max(bounds.maxVolume, 1);
2534
+ priceWindowRef.current = { min: minPrice, max: maxPrice };
2535
+ const chartWidth = Math.max(1, cssWidth - priceScaleWidth);
2536
+ const chartHeight = Math.max(1, cssHeight - timeScaleHeight - volumeHeight);
2537
+ const candleWidth = chartWidth / Math.max(candles.length, 1);
2538
+ // Generate price labels
2539
+ const priceLabelsArray = [];
2540
+ for (let i = 0; i <= 10; i++) {
2541
+ const price = minPrice + (priceRange * i) / 10;
2542
+ priceLabelsArray.push(numberFormatter.format(price));
2543
+ }
2544
+ setPriceLabels(priceLabelsArray);
2545
+ // Generate time labels
2546
+ const timeLabelsArray = [];
2547
+ const step = Math.max(1, Math.floor(candles.length / 10));
2548
+ for (let i = 0; i < candles.length; i += step) {
2549
+ const timestamp = candles[i]?.timestamp;
2550
+ if (timestamp !== undefined) {
2551
+ const date = new Date(timestamp);
2552
+ timeLabelsArray.push(timeFormatter.format(date));
2553
+ }
2554
+ else {
2555
+ timeLabelsArray.push(`Vela ${i} / Candle ${i}`);
2556
+ }
2557
+ }
2558
+ setTimeLabels(timeLabelsArray);
2559
+ // Draw grid
2560
+ const gridCtx = gridRef.current?.getContext('2d');
2561
+ if (gridCtx) {
2562
+ gridCtx.clearRect(0, 0, chartWidth, chartHeight);
2563
+ gridCtx.strokeStyle = '#363a45';
2564
+ gridCtx.lineWidth = 0.5;
2565
+ for (let i = 0; i <= 10; i++) {
2566
+ const y = alignStroke((i / 10) * chartHeight);
2567
+ gridCtx.beginPath();
2568
+ gridCtx.moveTo(0, y);
2569
+ gridCtx.lineTo(chartWidth, y);
2570
+ gridCtx.stroke();
2571
+ }
2572
+ const timeStep = Math.max(1, Math.floor(candles.length / 10));
2573
+ for (let i = 0; i <= candles.length; i += timeStep) {
2574
+ const x = alignStroke(i * candleWidth);
2575
+ gridCtx.beginPath();
2576
+ gridCtx.moveTo(x, 0);
2577
+ gridCtx.lineTo(x, chartHeight);
2578
+ gridCtx.stroke();
2579
+ }
2580
+ }
2581
+ const bullishColor = colorScheme === 'green' ? '#089981' : '#f23645';
2582
+ const bearishColor = colorScheme === 'green' ? '#f23645' : '#089981';
2583
+ // Draw price series
2584
+ const chartCtx = chartRef.current?.getContext('2d');
2585
+ if (chartCtx) {
2586
+ chartCtx.clearRect(0, 0, chartWidth, chartHeight);
2587
+ const isLineLike = seriesType === 'line' ||
2588
+ seriesType === 'line-markers' ||
2589
+ seriesType === 'step' ||
2590
+ seriesType === 'area' ||
2591
+ seriesType === 'hlc-area' ||
2592
+ seriesType === 'baseline';
2593
+ if (isLineLike) {
2594
+ // ----- LINE-BASED SERIES -----
2595
+ const baselinePrice = seriesType === 'baseline'
2596
+ ? (candles[0]?.close ?? minPrice)
2597
+ : minPrice;
2598
+ chartCtx.beginPath();
2599
+ candles.forEach((candle, index) => {
2600
+ const x = index * candleWidth + candleWidth / 2;
2601
+ const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
2602
+ if (index === 0) {
2603
+ chartCtx.moveTo(x, yClose);
2604
+ }
2605
+ else if (seriesType === 'step') {
2606
+ const prev = candles[index - 1];
2607
+ if (prev) {
2608
+ const prevY = ((maxPrice - prev.close) / priceRange) * chartHeight;
2609
+ chartCtx.lineTo(x, prevY);
2610
+ chartCtx.lineTo(x, yClose);
2611
+ }
2612
+ }
2613
+ else {
2614
+ chartCtx.lineTo(x, yClose);
2615
+ }
2616
+ });
2617
+ chartCtx.strokeStyle = '#2962ff';
2618
+ chartCtx.lineWidth = seriesType === 'baseline' ? 2 : 1;
2619
+ chartCtx.setLineDash([]);
2620
+ chartCtx.stroke();
2621
+ if (seriesType === 'line-markers') {
2622
+ candles.forEach((candle, index) => {
2623
+ const x = index * candleWidth + candleWidth / 2;
2624
+ const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
2625
+ chartCtx.beginPath();
2626
+ chartCtx.arc(x, yClose, 2.5, 0, 2 * Math.PI);
2627
+ chartCtx.fillStyle = '#2962ff';
2628
+ chartCtx.fill();
2629
+ });
2630
+ }
2631
+ if (seriesType === 'area' || seriesType === 'hlc-area' || seriesType === 'baseline') {
2632
+ chartCtx.lineTo((candles.length - 1) * candleWidth + candleWidth / 2, ((maxPrice - baselinePrice) / priceRange) * chartHeight);
2633
+ chartCtx.lineTo(0 * candleWidth + candleWidth / 2, ((maxPrice - baselinePrice) / priceRange) * chartHeight);
2634
+ chartCtx.closePath();
2635
+ chartCtx.fillStyle =
2636
+ seriesType === 'baseline' ? 'rgba(41,98,255,0.25)' : 'rgba(41,98,255,0.15)';
2637
+ chartCtx.fill();
2638
+ }
2639
+ }
2640
+ else {
2641
+ // ----- CANDLE / BAR-BASED SERIES -----
2642
+ candles.forEach((candle, index) => {
2643
+ const xCenter = index * candleWidth + candleWidth / 2;
2644
+ const strokeX = alignStroke(xCenter);
2645
+ const yHigh = ((maxPrice - candle.high) / priceRange) * chartHeight;
2646
+ const yLow = ((maxPrice - candle.low) / priceRange) * chartHeight;
2647
+ const yOpen = ((maxPrice - candle.open) / priceRange) * chartHeight;
2648
+ const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
2649
+ const alignedYOpen = alignStroke(yOpen);
2650
+ const alignedYClose = alignStroke(yClose);
2651
+ const alignedYHigh = alignStroke(yHigh);
2652
+ const alignedYLow = alignStroke(yLow);
2653
+ const isBullish = candle.close > candle.open;
2654
+ const bodyHeight = Math.abs(yClose - yOpen);
2655
+ const bodyY = Math.min(yOpen, yClose);
2656
+ const colorUp = bullishColor;
2657
+ const colorDown = bearishColor;
2658
+ const color = isBullish ? colorUp : colorDown;
2659
+ // Common wick for styles that use full OHLC
2660
+ const usesWick = seriesType === 'candles' ||
2661
+ seriesType === 'hollow' ||
2662
+ seriesType === 'bars' ||
2663
+ seriesType === 'columns' ||
2664
+ seriesType === 'high-low';
2665
+ if (usesWick) {
2666
+ chartCtx.strokeStyle = color;
2667
+ chartCtx.lineWidth = 1;
2668
+ chartCtx.beginPath();
2669
+ chartCtx.moveTo(strokeX, alignedYHigh);
2670
+ chartCtx.lineTo(strokeX, alignedYLow);
2671
+ chartCtx.stroke();
2672
+ }
2673
+ if (seriesType === 'candles' || seriesType === 'hollow') {
2674
+ chartCtx.lineWidth = 1;
2675
+ chartCtx.strokeStyle = color;
2676
+ if (seriesType === 'hollow' && isBullish) {
2677
+ // Hollow candle: sólo borde cuando es alcista
2678
+ chartCtx.fillStyle = 'transparent';
2679
+ }
2680
+ else {
2681
+ chartCtx.fillStyle = color;
2682
+ }
2683
+ if (bodyHeight > 1) {
2684
+ if (seriesType === 'hollow' && isBullish) {
2685
+ chartCtx.strokeRect(xCenter - candleWidth * 0.4, bodyY, candleWidth * 0.8, bodyHeight);
2686
+ }
2687
+ else {
2688
+ chartCtx.fillRect(xCenter - candleWidth * 0.4, bodyY, candleWidth * 0.8, bodyHeight);
2689
+ chartCtx.strokeRect(xCenter - candleWidth * 0.4, bodyY, candleWidth * 0.8, bodyHeight);
2690
+ }
2691
+ }
2692
+ else {
2693
+ chartCtx.beginPath();
2694
+ const bodyYStroke = alignStroke(yOpen);
2695
+ chartCtx.moveTo(xCenter - candleWidth * 0.4, bodyYStroke);
2696
+ chartCtx.lineTo(xCenter + candleWidth * 0.4, bodyYStroke);
2697
+ chartCtx.stroke();
2698
+ }
2699
+ }
2700
+ else if (seriesType === 'bars') {
2701
+ // OHLC bar style
2702
+ chartCtx.strokeStyle = color;
2703
+ chartCtx.lineWidth = 1;
2704
+ // wick already drawn; add open/close ticks
2705
+ chartCtx.beginPath();
2706
+ // open tick (left)
2707
+ chartCtx.moveTo(xCenter - candleWidth * 0.4, alignedYOpen);
2708
+ chartCtx.lineTo(xCenter, alignedYOpen);
2709
+ // close tick (right)
2710
+ chartCtx.moveTo(xCenter, alignedYClose);
2711
+ chartCtx.lineTo(xCenter + candleWidth * 0.4, alignedYClose);
2712
+ chartCtx.stroke();
2713
+ }
2714
+ else if (seriesType === 'columns') {
2715
+ // Column style (vertical bar from low to high)
2716
+ chartCtx.fillStyle = color;
2717
+ chartCtx.fillRect(xCenter - candleWidth * 0.3, yLow, candleWidth * 0.6, yHigh - yLow);
2718
+ }
2719
+ else ;
2720
+ });
2721
+ }
2722
+ }
2723
+ // Draw volume
2724
+ const volumeCtx = volumeRef.current?.getContext('2d');
2725
+ if (volumeCtx) {
2726
+ volumeCtx.clearRect(0, 0, chartWidth, volumeHeight);
2727
+ candles.forEach((candle, index) => {
2728
+ const x = index * candleWidth;
2729
+ const barHeight = ((candle.volume || 0) / maxVolume) * volumeHeight;
2730
+ const y = volumeHeight - barHeight;
2731
+ const isBullish = candle.close > candle.open;
2732
+ volumeCtx.fillStyle = isBullish ? bullishColor : bearishColor;
2733
+ volumeCtx.fillRect(x + 1, y, candleWidth - 2, barHeight);
2734
+ });
2735
+ }
2736
+ // Draw indicators
2737
+ const indicatorCtx = chartRef.current?.getContext('2d');
2738
+ if (indicatorCtx) {
2739
+ calculatedIndicators.forEach(indicator => {
2740
+ if (indicator.type === 'line') {
2741
+ indicatorCtx.strokeStyle = indicator.color;
2742
+ indicatorCtx.lineWidth = indicator.width || 1;
2743
+ indicatorCtx.setLineDash(indicator.style === 'dashed' ? [5, 5] : indicator.style === 'dotted' ? [2, 2] : []);
2744
+ indicatorCtx.beginPath();
2745
+ indicator.data.forEach((value, index) => {
2746
+ if (value !== null && value !== undefined) {
2747
+ const x = index * candleWidth + candleWidth / 2;
2748
+ const y = ((maxPrice - value) / priceRange) * chartHeight;
2749
+ if (index === 0) {
2750
+ indicatorCtx.moveTo(x, y);
2751
+ }
2752
+ else {
2753
+ indicatorCtx.lineTo(x, y);
2754
+ }
2755
+ }
2756
+ });
2757
+ indicatorCtx.stroke();
2758
+ }
2759
+ else if (indicator.type === 'histogram') {
2760
+ indicator.data.forEach((value, index) => {
2761
+ if (value !== null && value !== undefined) {
2762
+ const x = index * candleWidth;
2763
+ const barHeight = Math.abs(value) * chartHeight * 0.1; // Scale histogram
2764
+ const y = value >= 0 ? chartHeight / 2 - barHeight : chartHeight / 2;
2765
+ indicatorCtx.fillStyle = value >= 0 ? '#089981' : '#f23645';
2766
+ indicatorCtx.fillRect(x + 2, y, candleWidth - 4, barHeight);
2767
+ }
2768
+ });
2769
+ }
2770
+ });
2771
+ indicatorCtx.setLineDash([]); // Reset line dash
2772
+ }
2773
+ }, [visibleData, cssWidth, cssHeight, colorScheme, calculatedIndicators, seriesType, getVisibleCandles]);
2774
+ const drawCrosshair = React.useCallback(() => {
2775
+ const overlayCtx = overlayRef.current?.getContext('2d');
2776
+ if (overlayCtx && showCrosshairRef.current) {
2777
+ const { x, y } = mousePosRef.current;
2778
+ const alignedX = alignStroke(x);
2779
+ const alignedY = alignStroke(y);
2780
+ overlayCtx.clearRect(0, 0, chartWidth, overlayHeight);
2781
+ overlayCtx.strokeStyle = '#9598a1';
2782
+ overlayCtx.lineWidth = 1;
2783
+ overlayCtx.setLineDash([2, 2]);
2784
+ overlayCtx.beginPath();
2785
+ overlayCtx.moveTo(alignedX, 0);
2786
+ overlayCtx.lineTo(alignedX, chartHeight);
2787
+ overlayCtx.stroke();
2788
+ overlayCtx.beginPath();
2789
+ overlayCtx.moveTo(0, alignedY);
2790
+ overlayCtx.lineTo(chartWidth, alignedY);
2791
+ overlayCtx.stroke();
2792
+ overlayCtx.setLineDash([]);
2793
+ }
2794
+ }, [chartWidth, chartHeight, overlayHeight]);
2795
+ const handlePointerMove = React.useCallback((e) => {
2796
+ if (interactionsLocked || !visibleData.length)
2797
+ return;
2798
+ const rect = e.currentTarget.getBoundingClientRect();
2799
+ const x = e.clientX - rect.left;
2800
+ const y = e.clientY - rect.top;
2801
+ setClickedPrice(null);
2802
+ if (cursorType !== 'cross') {
2803
+ return;
2804
+ }
2805
+ if (!isDraggingRef.current) {
2806
+ const { min, max } = priceWindowRef.current;
2807
+ const snappedX = coordsRef.current.snapToCandle(x, chartWidth, visibleData.length);
2808
+ const snappedY = coordsRef.current.snapToPrice(y, chartHeight, min, max);
2809
+ mousePosRef.current = {
2810
+ x: coordsRef.current.timeToPixel(snappedX, chartWidth, visibleData.length),
2811
+ y: coordsRef.current.priceToPixel(snappedY, chartHeight, min, max)
2812
+ };
2813
+ showCrosshairRef.current = true;
2814
+ requestAnimationFrame(drawCrosshair);
2815
+ }
2816
+ }, [visibleData, cursorType, chartWidth, chartHeight, drawCrosshair, interactionsLocked]);
2817
+ const handleMouseLeave = React.useCallback(() => {
2818
+ showCrosshairRef.current = false;
2819
+ isDraggingRef.current = false;
2820
+ dragSampleRef.current.velocity = 0;
2821
+ dragSampleRef.current.lastTs = 0;
2822
+ requestAnimationFrame(() => {
2823
+ const overlayCtx = overlayRef.current?.getContext('2d');
2824
+ if (overlayCtx) {
2825
+ overlayCtx.clearRect(0, 0, chartWidth, overlayHeight);
2826
+ }
2827
+ });
2828
+ }, [chartWidth, overlayHeight]);
2829
+ const handleWheel = React.useCallback((e) => {
2830
+ if (interactionsLocked)
2831
+ return;
2832
+ e.preventDefault();
2833
+ applyPanDelta(e.deltaX);
2834
+ }, [interactionsLocked, applyPanDelta]);
2835
+ const handleQuickZoom = React.useCallback((direction) => {
2836
+ if (interactionsLocked)
2837
+ return;
2838
+ const factor = direction === 'in' ? 1.2 : 0.8;
2839
+ const targetZoom = clampZoomLevel(zoomLevelRef.current * factor);
2840
+ startZoomAnimation(targetZoom, 0.5);
2841
+ }, [interactionsLocked, startZoomAnimation, clampZoomLevel]);
2842
+ const handleResetView = React.useCallback(() => {
2843
+ if (interactionsLocked)
2844
+ return;
2845
+ cancelInertia();
2846
+ cancelZoomAnimation();
2847
+ setPanOffsetSafe(0);
2848
+ startZoomAnimation(1, 0.5);
2849
+ }, [interactionsLocked, cancelInertia, cancelZoomAnimation, setPanOffsetSafe, startZoomAnimation]);
2850
+ const getPointerPoint = React.useCallback((event) => {
2851
+ if (interactionsLocked)
2852
+ return null;
2853
+ const targetData = visibleData.length ? visibleData : storeData;
2854
+ if (!event.currentTarget || !targetData.length) {
2855
+ return null;
2856
+ }
2857
+ const rect = event.currentTarget.getBoundingClientRect();
2858
+ let x = event.clientX - rect.left;
2859
+ let y = event.clientY - rect.top;
2860
+ const usingVisible = visibleData.length > 0;
2861
+ const bounds = usingVisible
2862
+ ? { minPrice: priceWindowRef.current.min, maxPrice: priceWindowRef.current.max }
2863
+ : getDataBounds(targetData);
2864
+ const dataLength = Math.max(targetData.length, 1);
2865
+ if (magnetEnabled && visibleData.length) {
2866
+ const snappedIndex = coordsRef.current.snapToCandle(x, chartWidth, dataLength);
2867
+ const snappedPrice = coordsRef.current.snapToPrice(y, chartHeight, bounds.minPrice, bounds.maxPrice, 0.5);
2868
+ x = coordsRef.current.timeToPixel(snappedIndex, chartWidth, dataLength);
2869
+ y = coordsRef.current.priceToPixel(snappedPrice, chartHeight, bounds.minPrice, bounds.maxPrice);
2870
+ }
2871
+ const price = coordsRef.current.pixelToPrice(y, chartHeight, bounds.minPrice, bounds.maxPrice);
2872
+ const time = coordsRef.current.pixelToTime(x, chartWidth, dataLength);
2873
+ return { x, y, price, time };
2874
+ }, [visibleData, storeData, magnetEnabled, chartWidth, chartHeight, interactionsLocked]);
2875
+ const tryEraseDrawing = React.useCallback((x, y) => {
2876
+ for (let i = drawings.length - 1; i >= 0; i--) {
2877
+ const drawing = drawings[i];
2878
+ if (!drawing)
2879
+ continue;
2880
+ if (drawing.points.some(point => Math.hypot(point.x - x, point.y - y) < 14)) {
2881
+ deleteDrawing(drawing.id);
2882
+ showToast('Dibujo eliminado');
2883
+ return true;
2884
+ }
2885
+ }
2886
+ return false;
2887
+ }, [drawings, deleteDrawing, showToast]);
2888
+ const finalizeDrawing = React.useCallback((draft) => {
2889
+ const normalizedPoints = draft.points.length ? [...draft.points] : [];
2890
+ if (normalizedPoints.length === 1 &&
2891
+ draft.type !== 'text' &&
2892
+ draft.type !== 'icon' &&
2893
+ draft.type !== 'freehand') {
2894
+ const firstPoint = normalizedPoints[0];
2895
+ if (firstPoint) {
2896
+ normalizedPoints.push({ ...firstPoint });
2897
+ }
2898
+ }
2899
+ let normalized = {
2900
+ ...draft,
2901
+ points: normalizedPoints
2902
+ };
2903
+ const start = normalized.points[0];
2904
+ const end = normalized.points[1] ?? normalized.points[0];
2905
+ if (!start) {
2906
+ return normalized;
2907
+ }
2908
+ switch (draft.type) {
2909
+ case 'horizontal':
2910
+ case 'horizontal-line':
2911
+ case 'horizontal-ray': {
2912
+ if (end) {
2913
+ normalized.points = [start, { ...end, y: start.y, price: start.price }];
2914
+ }
2915
+ break;
2916
+ }
2917
+ case 'vertical-line': {
2918
+ if (end) {
2919
+ normalized.points = [start, { ...end, x: start.x, time: start.time }];
2920
+ }
2921
+ break;
2922
+ }
2923
+ case 'rectangle': {
2924
+ normalized.backgroundColor = draft.backgroundColor ?? 'rgba(96,165,250,0.15)';
2925
+ break;
2926
+ }
2927
+ case 'triangle': {
2928
+ if (end) {
2929
+ normalized.points = createTrianglePoints(start, end);
2930
+ }
2931
+ normalized.backgroundColor = draft.backgroundColor ?? 'rgba(244,114,182,0.12)';
2932
+ break;
2933
+ }
2934
+ case 'channel':
2935
+ case 'parallel':
2936
+ case 'regression-trend':
2937
+ case 'flat-top-bottom':
2938
+ case 'disjoint-channel':
2939
+ case 'pitchfork':
2940
+ case 'schiff-pitchfork':
2941
+ case 'modified-schiff-pitchfork':
2942
+ case 'inside-pitchfork': {
2943
+ if (end) {
2944
+ const offset = getChannelOffset(draft.type);
2945
+ normalized.points = createParallelPoints(start, end, offset);
2946
+ }
2947
+ if (['channel', 'regression-trend', 'flat-top-bottom', 'disjoint-channel'].includes(draft.type)) {
2948
+ normalized.backgroundColor = draft.backgroundColor ?? 'rgba(52,211,153,0.08)';
2949
+ }
2950
+ if (['pitchfork', 'schiff-pitchfork', 'modified-schiff-pitchfork', 'inside-pitchfork'].includes(draft.type)) {
2951
+ normalized.backgroundColor = draft.backgroundColor ?? 'rgba(244,114,182,0.08)';
2952
+ }
2953
+ break;
2954
+ }
2955
+ case 'cross-line': {
2956
+ if (end) {
2957
+ const horizontal = Math.max(20, Math.abs(end.x - start.x));
2958
+ const vertical = Math.max(20, Math.abs(end.y - start.y));
2959
+ normalized.meta = {
2960
+ ...normalized.meta,
2961
+ crossSize: { horizontal, vertical }
2962
+ };
2963
+ }
2964
+ break;
2965
+ }
2966
+ case 'ruler': {
2967
+ if (end) {
2968
+ normalized.meta = {
2969
+ ...normalized.meta,
2970
+ measurement: buildRulerMeta(start, end)
2971
+ };
2972
+ }
2973
+ break;
2974
+ }
2975
+ case 'icon': {
2976
+ normalized.icon = draft.icon ?? selectedEmoji ?? '⭐';
2977
+ normalized.fontSize = draft.fontSize ?? 22;
2978
+ normalized.color = draft.color ?? '#fcd34d';
2979
+ break;
2980
+ }
2981
+ case 'text': {
2982
+ normalized.fontSize = draft.fontSize ?? 14;
2983
+ normalized.backgroundColor = draft.backgroundColor ?? 'rgba(15,23,42,0.65)';
2984
+ normalized.color = draft.color ?? '#e5e7eb';
2985
+ break;
2986
+ }
2987
+ }
2988
+ return normalized;
2989
+ }, [selectedEmoji]);
2990
+ const handleSaveNote = React.useCallback(() => {
2991
+ if (!pendingNotePoint || !noteDraft.trim()) {
2992
+ setShowNoteModal(false);
2993
+ setPendingNotePoint(null);
2994
+ setNoteDraft('');
2995
+ return;
2996
+ }
2997
+ const textDrawing = {
2998
+ id: `drawing-${Date.now()}`,
2999
+ type: 'text',
3000
+ points: [pendingNotePoint],
3001
+ color: '#e5e7eb',
3002
+ width: 1.2,
3003
+ style: 'solid',
3004
+ locked: false,
3005
+ visible: true,
3006
+ text: noteDraft.trim(),
3007
+ backgroundColor: 'rgba(15,23,42,0.65)'
3008
+ };
3009
+ addDrawing(finalizeDrawing(textDrawing));
3010
+ setShowNoteModal(false);
3011
+ setPendingNotePoint(null);
3012
+ setNoteDraft('');
3013
+ showToast('Note added');
3014
+ }, [pendingNotePoint, noteDraft, addDrawing, finalizeDrawing, showToast]);
3015
+ const handleCancelNote = React.useCallback(() => {
3016
+ setShowNoteModal(false);
3017
+ setPendingNotePoint(null);
3018
+ setNoteDraft('');
3019
+ }, []);
3020
+ const handlePointerDown = React.useCallback((e) => {
3021
+ if (interactionsLocked)
3022
+ return;
3023
+ cancelInertia();
3024
+ cancelZoomAnimation();
3025
+ dragSampleRef.current.velocity = 0;
3026
+ dragSampleRef.current.lastTs = performance.now();
3027
+ const rect = e.currentTarget.getBoundingClientRect();
3028
+ const rawX = e.clientX - rect.left;
3029
+ const rawY = e.clientY - rect.top;
3030
+ if (activeTool !== 'cursor') {
3031
+ const point = getPointerPoint(e);
3032
+ if (!point)
3033
+ return;
3034
+ if (activeTool === 'text') {
3035
+ if (!point)
3036
+ return;
3037
+ setPendingNotePoint(point);
3038
+ setNoteDraft('');
3039
+ setShowNoteModal(true);
3040
+ return;
3041
+ }
3042
+ if (activeTool === 'icon') {
3043
+ const emoji = selectedEmoji ?? '⭐';
3044
+ if (!selectedEmoji) {
3045
+ setSelectedEmoji(emoji);
3046
+ }
3047
+ const iconDrawing = {
3048
+ id: `drawing-${Date.now()}`,
3049
+ type: 'icon',
3050
+ points: [point],
3051
+ color: '#fcd34d',
3052
+ width: 1,
3053
+ style: 'solid',
3054
+ locked: false,
3055
+ visible: true,
3056
+ icon: emoji
3057
+ };
3058
+ addDrawing(finalizeDrawing(iconDrawing));
3059
+ showToast('Icono añadido');
3060
+ return;
3061
+ }
3062
+ const isLineTool = LINE_TOOL_SET.has(activeTool);
3063
+ const baseColor = isLineTool ? strokeColor : (TOOL_COLOR_MAP[activeTool] ?? strokeColor);
3064
+ const baseWidth = isLineTool ? strokeWidth : (activeTool === 'freehand' ? 2.2 : 1.5);
3065
+ setIsDrawing(true);
3066
+ setCurrentDrawing({
3067
+ id: `drawing-${Date.now()}`,
3068
+ type: activeTool,
3069
+ points: [point],
3070
+ color: baseColor,
3071
+ width: baseWidth,
3072
+ style: 'solid',
3073
+ locked: false,
3074
+ visible: true,
3075
+ backgroundColor: activeTool === 'rectangle' ? 'rgba(96,165,250,0.15)' : undefined
3076
+ });
3077
+ return;
3078
+ }
3079
+ if (cursorType === 'eraser' && tryEraseDrawing(rawX, rawY)) {
3080
+ return;
3081
+ }
3082
+ if (visibleData.length) {
3083
+ const { min, max } = priceWindowRef.current;
3084
+ const snappedIndex = coordsRef.current.snapToCandle(rawX, chartWidth, visibleData.length);
3085
+ setSelectedCandleIndex(snappedIndex);
3086
+ const price = coordsRef.current.pixelToPrice(rawY, chartHeight, min, max);
3087
+ setClickedPrice({ x: rawX, y: rawY, price });
3088
+ }
3089
+ isDraggingRef.current = true;
3090
+ dragSampleRef.current.lastTs = performance.now();
3091
+ lastMouseXRef.current = e.clientX;
3092
+ }, [activeTool, cursorType, getPointerPoint, visibleData, chartWidth, chartHeight, addDrawing, finalizeDrawing, showToast, selectedEmoji, setSelectedEmoji, setIsDrawing, setCurrentDrawing, tryEraseDrawing, interactionsLocked, strokeColor, strokeWidth, cancelInertia, cancelZoomAnimation]);
3093
+ const handlePointerUp = React.useCallback(() => {
3094
+ if (isDrawing && currentDrawing) {
3095
+ const normalized = finalizeDrawing(currentDrawing);
3096
+ addDrawing(normalized);
3097
+ setIsDrawing(false);
3098
+ setCurrentDrawing(null);
3099
+ }
3100
+ const wasDragging = isDraggingRef.current;
3101
+ isDraggingRef.current = false;
3102
+ if (!isDrawing && !currentDrawing && wasDragging) {
3103
+ startInertia(dragSampleRef.current.velocity);
3104
+ }
3105
+ }, [isDrawing, currentDrawing, finalizeDrawing, addDrawing, setIsDrawing, setCurrentDrawing, startInertia]);
3106
+ const takeScreenshot = React.useCallback(() => {
3107
+ const priceCanvas = chartRef.current;
3108
+ if (!priceCanvas) {
3109
+ return;
3110
+ }
3111
+ const gridCanvas = gridRef.current;
3112
+ const volumeCanvas = volumeRef.current;
3113
+ const drawingsCanvas = drawingsRef.current;
3114
+ const overlayCanvas = overlayRef.current;
3115
+ const priceHeight = priceCanvas.height || 0;
3116
+ const volumeHeightPx = volumeCanvas?.height ?? 0;
3117
+ const overlayHeightPx = overlayCanvas?.height ?? priceHeight + volumeHeightPx;
3118
+ const exportWidth = priceCanvas.width || gridCanvas?.width || 0;
3119
+ const exportHeight = Math.max(priceHeight + volumeHeightPx, overlayHeightPx);
3120
+ if (!exportWidth || !exportHeight) {
3121
+ return;
3122
+ }
3123
+ const exportCanvas = document.createElement('canvas');
3124
+ exportCanvas.width = exportWidth;
3125
+ exportCanvas.height = exportHeight;
3126
+ const ctx = exportCanvas.getContext('2d');
3127
+ if (!ctx) {
3128
+ return;
3129
+ }
3130
+ ctx.fillStyle = '#020617';
3131
+ ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
3132
+ if (gridCanvas) {
3133
+ ctx.drawImage(gridCanvas, 0, 0, gridCanvas.width, gridCanvas.height);
3134
+ }
3135
+ ctx.drawImage(priceCanvas, 0, 0, priceCanvas.width, priceCanvas.height);
3136
+ if (volumeCanvas) {
3137
+ ctx.drawImage(volumeCanvas, 0, priceHeight, volumeCanvas.width, volumeCanvas.height);
3138
+ }
3139
+ if (drawingsCanvas) {
3140
+ ctx.drawImage(drawingsCanvas, 0, 0, drawingsCanvas.width, drawingsCanvas.height);
3141
+ }
3142
+ if (overlayCanvas) {
3143
+ ctx.drawImage(overlayCanvas, 0, 0, overlayCanvas.width, overlayCanvas.height);
3144
+ }
3145
+ const link = document.createElement('a');
3146
+ const normalizedSymbol = symbol.replace(/[^a-z0-9]+/gi, '-');
3147
+ link.download = `${normalizedSymbol}-${timeframe}-viainti.png`;
3148
+ link.href = exportCanvas.toDataURL('image/png');
3149
+ link.click();
3150
+ showToast(strings.toasts.screenshot);
3151
+ }, [showToast, strings.toasts.screenshot, symbol, timeframe]);
3152
+ const handlePointerMoveDrag = React.useCallback((e) => {
3153
+ if (interactionsLocked)
3154
+ return;
3155
+ if (isDrawing && currentDrawing) {
3156
+ const point = getPointerPoint(e);
3157
+ if (!point)
3158
+ return;
3159
+ if (!currentDrawing.points)
3160
+ return;
3161
+ const updatedPoints = [...currentDrawing.points];
3162
+ const isFreehand = currentDrawing.type === 'freehand';
3163
+ if (isFreehand) {
3164
+ updatedPoints.push(point);
3165
+ }
3166
+ else {
3167
+ updatedPoints[1] = point;
3168
+ }
3169
+ setCurrentDrawing({ ...currentDrawing, points: updatedPoints });
3170
+ return;
3171
+ }
3172
+ if (isDraggingRef.current) {
3173
+ const deltaX = e.clientX - lastMouseXRef.current;
3174
+ const now = performance.now();
3175
+ const lastTs = dragSampleRef.current.lastTs || now;
3176
+ const deltaTime = Math.max(1, now - lastTs);
3177
+ dragSampleRef.current.velocity = deltaX / deltaTime;
3178
+ dragSampleRef.current.lastTs = now;
3179
+ applyPanDelta(deltaX);
3180
+ lastMouseXRef.current = e.clientX;
3181
+ }
3182
+ }, [isDrawing, currentDrawing, getPointerPoint, setCurrentDrawing, interactionsLocked, applyPanDelta]);
3183
+ const referenceCandle = selectedCandleIndex !== null && visibleData[selectedCandleIndex]
3184
+ ? visibleData[selectedCandleIndex]
3185
+ : visibleData[visibleData.length - 1];
3186
+ const referenceTimestamp = referenceCandle?.timestamp ?? null;
3187
+ const currentTimeLabel = React.useMemo(() => {
3188
+ const source = referenceTimestamp ?? Date.now();
3189
+ return timeFormatter.format(new Date(source));
3190
+ }, [referenceTimestamp, timeFormatter]);
3191
+ const leftRailActions = React.useMemo(() => ([
3192
+ { key: 'zoomIn', icon: React.createElement(BsZoomIn, null), onClick: () => handleQuickZoom('in'), active: false, label: strings.actions.zoomIn },
3193
+ { key: 'zoomOut', icon: React.createElement(BsZoomOut, null), onClick: () => handleQuickZoom('out'), active: false, label: strings.actions.zoomOut },
3194
+ { key: 'lock', icon: React.createElement(BsLock, null), onClick: toggleInteractionsLock, active: interactionsLocked, label: interactionsLocked ? strings.actions.unlock : strings.actions.lock },
3195
+ { key: 'hide', icon: React.createElement(BsEyeSlash, null), onClick: toggleDrawingsHidden, active: drawingsHidden, label: drawingsHidden ? strings.actions.show : strings.actions.hide },
3196
+ { key: 'reset', icon: React.createElement(BsArrowRepeat, null), onClick: handleResetView, active: false, label: strings.actions.reset },
3197
+ { key: 'magnet', icon: React.createElement(BsMagnet, null), onClick: toggleMagnet, active: magnetEnabled, label: magnetEnabled ? strings.actions.magnetOn : strings.actions.magnetOff },
3198
+ { key: 'snapshot', icon: React.createElement(BsCamera, null), onClick: takeScreenshot, active: false, label: strings.actions.capture },
3199
+ { key: 'indicators', icon: React.createElement(BsBarChartSteps, null), onClick: toggleIndicatorsPanel, active: showIndicatorsPanel, label: strings.actions.indicators }
3200
+ ]), [handleQuickZoom, handleResetView, toggleInteractionsLock, toggleDrawingsHidden, toggleMagnet, toggleIndicatorsPanel, interactionsLocked, drawingsHidden, magnetEnabled, showIndicatorsPanel, takeScreenshot, strings.actions]);
3201
+ const seriesIconMap = {
3202
+ candles: React.createElement(BsBarChart, null),
3203
+ bars: React.createElement(BsBarChartSteps, null),
3204
+ hollow: React.createElement(BsBoundingBoxCircles, null),
3205
+ line: React.createElement(BsGraphUp, null),
3206
+ 'line-markers': React.createElement(BsGraphUp, null),
3207
+ step: React.createElement(BsBarChartSteps, null),
3208
+ area: React.createElement(BsCollection, null),
3209
+ 'hlc-area': React.createElement(BsCollection, null),
3210
+ baseline: React.createElement(BsGraphDown, null),
3211
+ columns: React.createElement(BsBarChartSteps, null),
3212
+ 'high-low': React.createElement(BsGraphDown, null)
3213
+ };
3214
+ const seriesTypeOptions = [
3215
+ { value: 'candles', label: 'Velas', icon: seriesIconMap.candles },
3216
+ { value: 'bars', label: 'Barras', icon: seriesIconMap.bars },
3217
+ { value: 'hollow', label: 'Hollow', icon: seriesIconMap.hollow },
3218
+ { value: 'line', label: 'Línea', icon: seriesIconMap.line },
3219
+ { value: 'line-markers', label: 'Línea + puntos', icon: seriesIconMap['line-markers'] },
3220
+ { value: 'step', label: 'Step', icon: seriesIconMap.step },
3221
+ { value: 'area', label: 'Área', icon: seriesIconMap.area },
3222
+ { value: 'hlc-area', label: 'HLC Área', icon: seriesIconMap['hlc-area'] },
3223
+ { value: 'baseline', label: 'Baseline', icon: seriesIconMap.baseline },
3224
+ { value: 'columns', label: 'Columnas', icon: seriesIconMap.columns },
3225
+ { value: 'high-low', label: 'High/Low', icon: seriesIconMap['high-low'] }
3226
+ ];
3227
+ const cursorCss = isDraggingRef.current
3228
+ ? 'grabbing'
3229
+ : cursorType === 'cross'
3230
+ ? 'crosshair'
3231
+ : cursorType === 'dot'
3232
+ ? 'crosshair'
3233
+ : cursorType === 'arrow'
3234
+ ? 'default'
3235
+ : cursorType === 'eraser'
3236
+ ? 'crosshair'
3237
+ : 'default';
3238
+ return (React.createElement("div", { ref: containerRef, style: {
3239
+ width: isFullscreen ? '100vw' : '100%',
3240
+ height: isFullscreen ? '100vh' : '100%',
3241
+ minHeight: isFullscreen ? '100vh' : '640px',
3242
+ background: activeTheme.pageBg,
3243
+ color: activeTheme.textPrimary,
3244
+ fontFamily: 'Inter, sans-serif',
3245
+ display: 'flex',
3246
+ flexDirection: 'column',
3247
+ position: isFullscreen ? 'fixed' : 'relative',
3248
+ inset: isFullscreen ? 0 : undefined,
3249
+ zIndex: isFullscreen ? 9999 : 'auto'
3250
+ } },
3251
+ React.createElement(framerMotion.motion.div, { style: {
3252
+ background: `linear-gradient(120deg, ${activeTheme.heroFrom} 0%, ${activeTheme.heroTo} 100%)`,
3253
+ borderBottom: `1px solid ${activeTheme.panelBorder}`,
3254
+ display: 'flex',
3255
+ flexWrap: 'wrap',
3256
+ gap: isMobile ? '12px' : '16px',
3257
+ alignItems: 'center',
3258
+ padding: isMobile ? '12px 16px' : '16px 20px',
3259
+ boxShadow: elevatedShadow
3260
+ }, initial: { opacity: 0, y: -15 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.3 } },
3261
+ React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center', minWidth: 0 } },
3262
+ React.createElement("div", { style: { background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}`, borderRadius: '14px', padding: '10px 16px', minWidth: '160px', color: activeTheme.textPrimary } },
3263
+ React.createElement("div", { style: { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.08em', color: activeTheme.textSecondary, marginBottom: '4px' } }, strings.symbolLabel),
3264
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: activeTheme.textPrimary, fontWeight: 600, gap: '10px' } },
3265
+ React.createElement("span", { style: { fontSize: '15px' } }, symbol),
3266
+ React.createElement("span", { style: { fontSize: '12px', background: activeTheme.accent, color: '#fff', padding: '2px 10px', borderRadius: '999px' } }, timeframe))),
3267
+ React.createElement(framerMotion.motion.div, { key: currentPrice, style: { background: activeTheme.cardBg, border: `1px solid ${priceChange >= 0 ? '#16a34a' : '#f43f5e'}`, borderRadius: '14px', padding: '10px 20px', display: 'flex', alignItems: 'center', gap: '16px', minWidth: '210px', color: activeTheme.textPrimary }, initial: { scale: 1.05, opacity: 0 }, animate: { scale: 1, opacity: 1 }, transition: { duration: 0.2 } },
3268
+ React.createElement("div", { style: { fontSize: '20px', fontWeight: 700, color: activeTheme.textPrimary } }, numberFormatter.format(currentPrice)),
3269
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
3270
+ React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: activeTheme.textSecondary } }, strings.priceChangeLabel),
3271
+ React.createElement(framerMotion.motion.span, { key: priceChange, style: {
3272
+ fontSize: '12px',
3273
+ fontWeight: 600,
3274
+ color: priceChange >= 0 ? '#16a34a' : '#f43f5e',
3275
+ background: themePreset === 'light' ? 'rgba(15,23,42,0.08)' : 'rgba(15,23,42,0.5)',
3276
+ padding: '4px 10px',
3277
+ borderRadius: '999px'
3278
+ }, initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.2 } },
3279
+ priceChange >= 0 ? '+' : '',
3280
+ numberFormatter.format(priceChange),
3281
+ " (",
3282
+ priceChangePercent >= 0 ? '+' : '',
3283
+ shortNumberFormatter.format(priceChangePercent),
3284
+ "%)"))),
3285
+ referenceCandle && (React.createElement("div", { style: { display: 'flex', gap: '10px', flexWrap: 'wrap', background: 'rgba(15,23,42,0.6)', borderRadius: '12px', padding: '10px 14px', border: '1px solid #1f2937', color: '#94a3b8', fontSize: '12px' } }, [
3286
+ { label: 'O', value: referenceCandle.open },
3287
+ { label: 'H', value: referenceCandle.high },
3288
+ { label: 'L', value: referenceCandle.low },
3289
+ { label: 'C', value: referenceCandle.close },
3290
+ { label: 'V', value: referenceCandle.volume ?? 0 }
3291
+ ].map(item => (React.createElement("div", { key: item.label, style: { display: 'flex', flexDirection: 'column', minWidth: '50px' } },
3292
+ React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase' } }, item.label),
3293
+ React.createElement("span", { style: { color: '#e2e8f0', fontWeight: 600 } }, item.value.toFixed(2)))))))),
3294
+ React.createElement("div", { style: {
3295
+ display: 'flex',
3296
+ alignItems: 'center',
3297
+ gap: '12px',
3298
+ padding: '10px 14px',
3299
+ background: activeTheme.panelBg,
3300
+ borderRadius: '16px',
3301
+ border: `1px solid ${activeTheme.panelBorder}`,
3302
+ overflowX: 'auto'
3303
+ } },
3304
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' } },
3305
+ React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, strings.timeframeTitle),
3306
+ React.createElement("span", { style: { fontSize: '11px', color: '#f8fafc', fontWeight: 600, padding: '2px 8px', borderRadius: '999px', background: 'rgba(37,99,235,0.25)', border: '1px solid rgba(37,99,235,0.6)' } }, timeframe)),
3307
+ React.createElement("div", { style: { display: 'flex', gap: '6px', flex: 1, minWidth: 0, overflowX: 'auto', paddingBottom: '4px' } }, ['1m', '3m', '5m', '15m', '30m', '1h', '4h'].map(tf => (React.createElement("button", { key: tf, onClick: () => handleTimeframeChange(tf), style: {
3308
+ background: timeframe === tf ? activeTheme.accent : activeTheme.panelBg,
3309
+ color: timeframe === tf ? '#f8fafc' : activeTheme.textSecondary,
3310
+ border: `1px solid ${timeframe === tf ? activeTheme.accent : activeTheme.panelBorder}`,
3311
+ borderRadius: '10px',
3312
+ padding: '6px 12px',
3313
+ fontSize: '12px',
3314
+ fontWeight: 600,
3315
+ cursor: 'pointer',
3316
+ transition: 'all 0.2s',
3317
+ flex: '0 0 auto',
3318
+ whiteSpace: 'nowrap'
3319
+ } }, tf)))),
3320
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: '#cbd5f5', whiteSpace: 'nowrap', flexShrink: 0 } },
3321
+ React.createElement(BsClockHistory, null),
3322
+ React.createElement("span", null,
3323
+ strings.axis.time,
3324
+ ": ",
3325
+ currentTimeLabel)),
3326
+ React.createElement("select", { value: timeframe, onChange: (e) => handleTimeframeChange(e.target.value), style: {
3327
+ background: 'rgba(15,23,42,0.8)',
3328
+ color: '#e2e8f0',
3329
+ border: '1px solid #1f2937',
3330
+ padding: '10px 36px 10px 12px',
3331
+ borderRadius: '12px',
3332
+ cursor: 'pointer',
3333
+ fontSize: '12px',
3334
+ fontWeight: 600,
3335
+ appearance: 'none',
3336
+ backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2394a3b8' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e")`,
3337
+ backgroundPosition: 'right 10px center',
3338
+ backgroundRepeat: 'no-repeat',
3339
+ backgroundSize: '14px',
3340
+ minWidth: '120px',
3341
+ flexShrink: 0
3342
+ } },
3343
+ React.createElement("option", { value: "1m" }, "1m"),
3344
+ React.createElement("option", { value: "3m" }, "3m"),
3345
+ React.createElement("option", { value: "5m" }, "5m"),
3346
+ React.createElement("option", { value: "15m" }, "15m"),
3347
+ React.createElement("option", { value: "30m" }, "30m"),
3348
+ React.createElement("option", { value: "1h" }, "1h"),
3349
+ React.createElement("option", { value: "4h" }, "4h"),
3350
+ React.createElement("option", { value: "12h" }, "12h"),
3351
+ React.createElement("option", { value: "1D" }, "1D"),
3352
+ React.createElement("option", { value: "3D" }, "3D"),
3353
+ React.createElement("option", { value: "1W" }, "1W"),
3354
+ React.createElement("option", { value: "1M" }, "1M"))),
3355
+ React.createElement("div", { style: { marginLeft: 'auto', display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
3356
+ React.createElement("button", { onClick: () => setShowSeriesMenu(!showSeriesMenu), style: {
3357
+ width: '38px',
3358
+ height: '38px',
3359
+ borderRadius: '999px',
3360
+ border: `1px solid ${showSeriesMenu ? activeTheme.accent : iconBaseBg}`,
3361
+ background: showSeriesMenu ? activeTheme.accent : iconBaseBg,
3362
+ color: iconBaseColor,
3363
+ display: 'flex',
3364
+ alignItems: 'center',
3365
+ justifyContent: 'center',
3366
+ cursor: 'pointer',
3367
+ transition: 'all 0.2s'
3368
+ }, title: strings.buttons.series, "aria-label": strings.buttons.series }, seriesIconMap[seriesType] ?? React.createElement(BsGraphUp, null)),
3369
+ React.createElement("button", { onClick: toggleIndicatorsPanel, style: {
3370
+ background: showIndicatorsPanel ? '#2563eb' : 'rgba(15,23,42,0.6)',
3371
+ color: '#e2e8f0',
3372
+ border: '1px solid #1f2937',
3373
+ borderRadius: '999px',
3374
+ padding: '8px 14px',
3375
+ fontSize: '12px',
3376
+ fontWeight: 600,
3377
+ cursor: 'pointer',
3378
+ transition: 'all 0.2s'
3379
+ } }, strings.buttons.indicators),
3380
+ React.createElement("button", { onClick: () => setShowConfigPanel(!showConfigPanel), style: {
3381
+ width: '38px',
3382
+ height: '38px',
3383
+ borderRadius: '999px',
3384
+ border: `1px solid ${showConfigPanel ? activeTheme.accent : iconBaseBg}`,
3385
+ background: showConfigPanel ? activeTheme.accent : iconBaseBg,
3386
+ color: iconBaseColor,
3387
+ display: 'flex',
3388
+ alignItems: 'center',
3389
+ justifyContent: 'center',
3390
+ cursor: 'pointer',
3391
+ transition: 'all 0.2s'
3392
+ }, title: strings.buttons.config, "aria-label": strings.buttons.config },
3393
+ React.createElement(BsGear, null)),
3394
+ React.createElement("button", { onClick: () => setIsFullscreen(prev => !prev), style: {
3395
+ width: '38px',
3396
+ height: '38px',
3397
+ borderRadius: '999px',
3398
+ border: `1px solid ${isFullscreen ? activeTheme.accent : iconBaseBg}`,
3399
+ background: isFullscreen ? activeTheme.accent : iconBaseBg,
3400
+ color: iconBaseColor,
3401
+ display: 'flex',
3402
+ alignItems: 'center',
3403
+ justifyContent: 'center',
3404
+ cursor: 'pointer',
3405
+ transition: 'all 0.2s'
3406
+ }, title: isFullscreen ? strings.buttons.fullscreenExit : strings.buttons.fullscreenEnter, "aria-label": isFullscreen ? strings.buttons.fullscreenExit : strings.buttons.fullscreenEnter },
3407
+ React.createElement(BsArrowsFullscreen, null)),
3408
+ React.createElement("button", { onClick: takeScreenshot, style: {
3409
+ width: '38px',
3410
+ height: '38px',
3411
+ borderRadius: '999px',
3412
+ border: `1px solid ${iconBaseBg}`,
3413
+ background: iconBaseBg,
3414
+ color: iconBaseColor,
3415
+ display: 'flex',
3416
+ alignItems: 'center',
3417
+ justifyContent: 'center',
3418
+ cursor: 'pointer',
3419
+ transition: 'all 0.2s'
3420
+ }, title: strings.buttons.screenshot, "aria-label": strings.buttons.screenshot },
3421
+ React.createElement(BsCamera, null)),
3422
+ React.createElement("button", { onClick: () => setShowQuickTips(prev => !prev), style: {
3423
+ width: '38px',
3424
+ height: '38px',
3425
+ borderRadius: '999px',
3426
+ border: `1px solid ${showQuickTips ? activeTheme.accent : iconBaseBg}`,
3427
+ background: showQuickTips ? activeTheme.accent : iconBaseBg,
3428
+ color: iconBaseColor,
3429
+ display: 'flex',
3430
+ alignItems: 'center',
3431
+ justifyContent: 'center',
3432
+ cursor: 'pointer',
3433
+ transition: 'all 0.2s'
3434
+ }, title: strings.buttons.help, "aria-label": strings.buttons.help },
3435
+ React.createElement(BsQuestionCircle, null)))),
3436
+ derivedStats && (React.createElement("div", { style: { padding: '16px 20px 0', background: activeTheme.panelBg, borderBottom: `1px solid ${activeTheme.panelBorder}` } },
3437
+ React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '12px' } },
3438
+ React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
3439
+ React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Trading range"),
3440
+ React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } }, numberFormatter.format(derivedStats.range)),
3441
+ React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } },
3442
+ derivedStats.rangePct.toFixed(2),
3443
+ "%")),
3444
+ React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
3445
+ React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Avg volume"),
3446
+ React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } }, shortNumberFormatter.format(derivedStats.avgVolume)),
3447
+ React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } },
3448
+ "Last ",
3449
+ derivedStats.samples,
3450
+ " samples")),
3451
+ React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
3452
+ React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Volatility"),
3453
+ React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } },
3454
+ derivedStats.volatility.toFixed(2),
3455
+ "%"),
3456
+ React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } }, "Abs move mean")),
3457
+ React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
3458
+ React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Session"),
3459
+ React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } }, derivedStats.session),
3460
+ React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } },
3461
+ "Latency ~",
3462
+ latencyMs,
3463
+ "ms"))))),
3464
+ React.createElement(framerMotion.AnimatePresence, null, showQuickTips && (React.createElement(framerMotion.motion.div, { initial: { opacity: 0, y: -10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, transition: { duration: 0.2 }, style: {
3465
+ position: (isFullscreen ? 'fixed' : 'absolute'),
3466
+ top: isFullscreen ? 80 : 140,
3467
+ left: isFullscreen ? 80 : 140,
3468
+ maxWidth: '300px',
3469
+ background: activeTheme.overlayBg,
3470
+ borderRadius: '16px',
3471
+ padding: '16px',
3472
+ border: `1px solid ${activeTheme.accent}`,
3473
+ boxShadow: '0 25px 60px rgba(0,0,0,0.55)',
3474
+ zIndex: 1200
3475
+ } },
3476
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px', color: activeTheme.textPrimary, fontWeight: 600 } },
3477
+ React.createElement(BsInfoCircle, null),
3478
+ strings.quickTipsTitle),
3479
+ React.createElement("ul", { style: { listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px', color: activeTheme.textSecondary, fontSize: '13px' } }, strings.quickTips.map(tip => (React.createElement("li", { key: tip, style: { display: 'flex', alignItems: 'center', gap: '6px' } },
3480
+ React.createElement(BsCheck2, { style: { color: activeTheme.accent } }),
3481
+ React.createElement("span", null, tip))))),
3482
+ React.createElement("button", { onClick: () => setShowQuickTips(false), style: {
3483
+ marginTop: '12px',
3484
+ width: '100%',
3485
+ background: activeTheme.accent,
3486
+ color: '#f8fafc',
3487
+ border: 'none',
3488
+ borderRadius: '10px',
3489
+ padding: '8px 0',
3490
+ fontSize: '12px',
3491
+ fontWeight: 600,
3492
+ cursor: 'pointer'
3493
+ } }, strings.quickTipsButton)))),
3494
+ showNoteModal && (React.createElement("div", { style: {
3495
+ position: (isFullscreen ? 'fixed' : 'absolute'),
3496
+ inset: isFullscreen ? 0 : undefined,
3497
+ top: isFullscreen ? 0 : 0,
3498
+ left: isFullscreen ? 0 : 0,
3499
+ right: 0,
3500
+ bottom: 0,
3501
+ background: activeTheme.overlayBg,
3502
+ display: 'flex',
3503
+ alignItems: 'center',
3504
+ justifyContent: 'center',
3505
+ zIndex: 1600,
3506
+ padding: '20px'
3507
+ } },
3508
+ React.createElement("div", { style: {
3509
+ width: 'min(420px, 90vw)',
3510
+ background: activeTheme.panelBg,
3511
+ borderRadius: '20px',
3512
+ border: `1px solid ${activeTheme.panelBorder}`,
3513
+ padding: '24px',
3514
+ boxShadow: '0 30px 80px rgba(0,0,0,0.6)',
3515
+ display: 'flex',
3516
+ flexDirection: 'column',
3517
+ gap: '12px'
3518
+ } },
3519
+ React.createElement("div", null,
3520
+ React.createElement("p", { style: { margin: 0, fontSize: '12px', letterSpacing: '0.08em', textTransform: 'uppercase', color: activeTheme.textSecondary } }, "Quick note"),
3521
+ React.createElement("h3", { style: { margin: '4px 0 0', color: activeTheme.textPrimary, fontSize: '18px' } }, "Write a quick note in English")),
3522
+ React.createElement("textarea", { value: noteDraft, onChange: (event) => setNoteDraft(event.target.value), placeholder: "Describe your idea...", rows: 4, style: {
3523
+ width: '100%',
3524
+ background: activeTheme.plotBg,
3525
+ border: `1px solid ${activeTheme.panelBorder}`,
3526
+ borderRadius: '12px',
3527
+ padding: '12px',
3528
+ color: activeTheme.textPrimary,
3529
+ fontFamily: 'Inter, sans-serif',
3530
+ fontSize: '14px',
3531
+ resize: 'none'
3532
+ } }),
3533
+ React.createElement("div", { style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' } },
3534
+ React.createElement("button", { onClick: handleCancelNote, style: {
3535
+ padding: '8px 16px',
3536
+ borderRadius: '10px',
3537
+ border: `1px solid ${activeTheme.panelBorder}`,
3538
+ background: 'transparent',
3539
+ color: activeTheme.textPrimary,
3540
+ cursor: 'pointer',
3541
+ fontSize: '13px',
3542
+ fontWeight: 600
3543
+ } }, "Cancel"),
3544
+ React.createElement("button", { onClick: handleSaveNote, disabled: !noteDraft.trim(), style: {
3545
+ padding: '8px 16px',
3546
+ borderRadius: '10px',
3547
+ border: 'none',
3548
+ background: noteDraft.trim() ? activeTheme.accent : activeTheme.accentSoft,
3549
+ color: '#f8fafc',
3550
+ cursor: noteDraft.trim() ? 'pointer' : 'not-allowed',
3551
+ fontSize: '13px',
3552
+ fontWeight: 600
3553
+ } }, "Save note"))))),
3554
+ React.createElement("div", { style: {
3555
+ flex: 1,
3556
+ display: 'flex',
3557
+ gap: isMobile ? '12px' : '16px',
3558
+ padding: isFullscreen ? '16px' : isMobile ? '12px 16px 20px' : '16px 20px 24px',
3559
+ minHeight: 0,
3560
+ alignItems: 'stretch'
3561
+ } },
3562
+ React.createElement("div", { style: { width: isMobile ? '60px' : '88px', flexShrink: 0, height: '100%' } },
3563
+ React.createElement("div", { style: {
3564
+ height: '100%',
3565
+ borderRadius: '24px',
3566
+ border: `1px solid ${themePreset === 'light' ? '#0f172a' : activeTheme.railBorder}`,
3567
+ background: themePreset === 'light' ? '#0f172a' : activeTheme.railBg,
3568
+ display: 'flex',
3569
+ justifyContent: 'center',
3570
+ alignItems: 'center',
3571
+ padding: isMobile ? '12px 0' : '16px 0'
3572
+ } },
3573
+ React.createElement(DrawingToolbar, null))),
3574
+ React.createElement("div", { ref: plotAreaRef, style: {
3575
+ flex: 1,
3576
+ position: 'relative',
3577
+ background: activeTheme.plotBg,
3578
+ cursor: cursorCss,
3579
+ borderRadius: '24px',
3580
+ border: `1px solid ${activeTheme.plotBorder}`,
3581
+ overflow: 'hidden',
3582
+ minHeight: isMobile ? '400px' : '520px',
3583
+ boxShadow: surfaceShadow
3584
+ }, onPointerMove: (e) => { handlePointerMove(e); handlePointerMoveDrag(e); }, onPointerLeave: handleMouseLeave, onWheel: handleWheel, onPointerDown: handlePointerDown, onPointerUp: handlePointerUp },
3585
+ React.createElement("div", { style: { position: 'absolute', left: 20, bottom: 90, display: 'flex', flexDirection: 'column', gap: '10px', zIndex: 35 } }, leftRailActions.map(action => {
3586
+ const isDisabled = interactionsLocked && ['zoomIn', 'zoomOut', 'reset'].includes(action.key);
3587
+ return (React.createElement("button", { key: action.key, onClick: action.onClick, style: {
3588
+ width: isMobile ? '36px' : '42px',
3589
+ height: isMobile ? '36px' : '42px',
3590
+ borderRadius: '12px',
3591
+ border: `1px solid ${action.active ? activeTheme.accent : leftRailBaseBg}`,
3592
+ background: action.active ? activeTheme.accent : leftRailBaseBg,
3593
+ color: action.active ? '#f8fafc' : (themePreset === 'light' ? '#f8fafc' : activeTheme.textPrimary),
3594
+ display: 'flex',
3595
+ alignItems: 'center',
3596
+ justifyContent: 'center',
3597
+ fontSize: isMobile ? '16px' : '18px',
3598
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
3599
+ opacity: isDisabled ? 0.4 : 1,
3600
+ transition: 'all 0.2s'
3601
+ }, title: action.label, disabled: isDisabled }, action.icon));
3602
+ })),
3603
+ React.createElement("div", { style: { position: 'absolute', right: 0, top: 0, width: `${priceScaleWidth}px`, height: '100%', background: activeTheme.scaleBg, borderLeft: `1px solid ${activeTheme.plotBorder}`, fontSize: '11px', color: activeTheme.textSecondary, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '8px 0' } },
3604
+ React.createElement("div", { style: { textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.08em', paddingLeft: '5px', marginBottom: '6px' } }, strings.axis.price),
3605
+ priceLabels.map((label, i) => (React.createElement("div", { key: i, style: { textAlign: 'left', paddingLeft: '5px', color: activeTheme.textPrimary } }, label)))),
3606
+ React.createElement("canvas", { ref: gridRef, width: chartWidth, height: chartHeight, style: { position: 'absolute', left: 0, top: 0, width: chartWidth, height: chartHeight } }),
3607
+ React.createElement("canvas", { ref: chartRef, width: chartWidth, height: chartHeight, style: { position: 'absolute', left: 0, top: 0, width: chartWidth, height: chartHeight } }),
3608
+ React.createElement("canvas", { ref: volumeRef, width: chartWidth, height: volumeHeight, style: { position: 'absolute', left: 0, top: chartHeight, width: chartWidth, height: volumeHeight } }),
3609
+ React.createElement("canvas", { ref: drawingsRef, width: chartWidth, height: overlayHeight, style: { position: 'absolute', left: 0, top: 0, pointerEvents: 'none', width: chartWidth, height: overlayHeight } }),
3610
+ React.createElement("canvas", { ref: overlayRef, width: chartWidth, height: overlayHeight, style: { position: 'absolute', left: 0, top: 0, pointerEvents: 'none', width: chartWidth, height: overlayHeight } }),
3611
+ React.createElement("div", { style: { position: 'absolute', bottom: 0, left: 0, right: priceScaleWidth, height: `${timeScaleHeight}px`, background: activeTheme.scaleBg, borderTop: `1px solid ${activeTheme.plotBorder}`, fontSize: '11px', color: activeTheme.textSecondary, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: '4px', padding: '4px 10px' } },
3612
+ React.createElement("span", { style: { textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.08em' } }, strings.axis.time),
3613
+ React.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: activeTheme.textPrimary } }, timeLabels.map((label, i) => (React.createElement("div", { key: i }, label))))),
3614
+ clickedPrice && (React.createElement("div", { style: {
3615
+ position: 'absolute',
3616
+ background: '#1e222d',
3617
+ color: '#d1d4dc',
3618
+ padding: '8px 12px',
3619
+ borderRadius: '6px',
3620
+ fontSize: '14px',
3621
+ fontWeight: '600',
3622
+ pointerEvents: 'none',
3623
+ zIndex: 10,
3624
+ left: clickedPrice.x + 10,
3625
+ top: clickedPrice.y - 10,
3626
+ border: '1px solid #2a2e39',
3627
+ boxShadow: '0 4px 12px rgba(0,0,0,0.6)',
3628
+ backdropFilter: 'blur(8px)',
3629
+ WebkitBackdropFilter: 'blur(8px)'
3630
+ } },
3631
+ "$",
3632
+ clickedPrice.price.toFixed(2))),
3633
+ isSeriesLoading && (React.createElement(framerMotion.motion.div, { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 }, style: {
3634
+ position: 'absolute',
3635
+ inset: 0,
3636
+ display: 'flex',
3637
+ alignItems: 'center',
3638
+ justifyContent: 'center',
3639
+ background: 'rgba(0,0,0,0.35)',
3640
+ zIndex: 50
3641
+ } },
3642
+ React.createElement("div", { style: {
3643
+ padding: '10px 16px',
3644
+ borderRadius: '999px',
3645
+ background: '#111827',
3646
+ border: '1px solid #2962ff',
3647
+ display: 'flex',
3648
+ alignItems: 'center',
3649
+ gap: '8px',
3650
+ boxShadow: '0 10px 30px rgba(0,0,0,0.8)'
3651
+ } },
3652
+ React.createElement("div", { style: {
3653
+ width: '10px',
3654
+ height: '10px',
3655
+ borderRadius: '999px',
3656
+ background: '#2962ff',
3657
+ boxShadow: '0 0 12px #2962ff'
3658
+ } }),
3659
+ React.createElement("span", { style: { fontSize: '12px', color: '#e5e7eb', fontWeight: 500 } }, "Updating series...")))))),
3660
+ React.createElement(framerMotion.AnimatePresence, null, toastMessage && (React.createElement(framerMotion.motion.div, { key: toastMessage, initial: { opacity: 0, y: 12 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 12 }, transition: { duration: 0.2 }, style: {
3661
+ position: (isFullscreen ? 'fixed' : 'absolute'),
3662
+ bottom: isFullscreen ? 24 : 16,
3663
+ right: isFullscreen ? 24 : 16,
3664
+ background: 'rgba(15,23,42,0.9)',
3665
+ border: '1px solid #1d4ed8',
3666
+ borderRadius: '999px',
3667
+ padding: '10px 18px',
3668
+ color: '#e2e8f0',
3669
+ fontSize: '12px',
3670
+ display: 'flex',
3671
+ alignItems: 'center',
3672
+ gap: '8px',
3673
+ boxShadow: '0 20px 45px rgba(0,0,0,0.5)',
3674
+ pointerEvents: 'none',
3675
+ zIndex: 1200
3676
+ } },
3677
+ React.createElement(BsInfoCircle, { style: { color: '#60a5fa' } }),
3678
+ React.createElement("span", null, toastMessage)))),
3679
+ showSeriesMenu && (React.createElement("div", { style: {
3680
+ position: 'absolute',
3681
+ top: '50px',
3682
+ right: '140px',
3683
+ background: activeTheme.panelBg,
3684
+ border: `1px solid ${activeTheme.panelBorder}`,
3685
+ borderRadius: '12px',
3686
+ padding: '16px',
3687
+ minWidth: '240px',
3688
+ maxHeight: '60vh',
3689
+ overflowY: 'auto',
3690
+ zIndex: 1000,
3691
+ boxShadow: '0 12px 30px rgba(0,0,0,0.6)'
3692
+ } },
3693
+ React.createElement("div", { style: { fontSize: '13px', fontWeight: '600', marginBottom: '12px', color: '#d1d4dc', display: 'flex', alignItems: 'center', gap: '6px' } },
3694
+ React.createElement(BsGraphUp, null),
3695
+ "Series Type"),
3696
+ React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, seriesTypeOptions.map(({ value, label, icon }) => (React.createElement("button", { key: value, onClick: () => {
3697
+ setSeriesType(value);
3698
+ setShowSeriesMenu(false);
3699
+ }, style: {
3700
+ display: 'flex',
3701
+ alignItems: 'center',
3702
+ gap: '8px',
3703
+ padding: '8px',
3704
+ borderRadius: '10px',
3705
+ border: seriesType === value ? '1px solid #2563eb' : '1px solid #2a2e39',
3706
+ background: seriesType === value ? 'rgba(37,99,235,0.15)' : 'rgba(2,6,23,0.5)',
3707
+ color: '#e2e8f0',
3708
+ cursor: 'pointer',
3709
+ fontSize: '12px'
3710
+ } },
3711
+ React.createElement("span", { style: { fontSize: '18px' } }, icon),
3712
+ React.createElement("span", null, label))))))),
3713
+ showIndicatorsPanel && (React.createElement("div", { style: {
3714
+ position: 'absolute',
3715
+ top: '50px',
3716
+ right: '10px',
3717
+ background: activeTheme.panelBg,
3718
+ border: `1px solid ${activeTheme.panelBorder}`,
3719
+ borderRadius: '12px',
3720
+ padding: '16px',
3721
+ minWidth: '250px',
3722
+ maxHeight: '60vh',
3723
+ overflowY: 'auto',
3724
+ zIndex: 1000,
3725
+ boxShadow: '0 8px 24px rgba(0,0,0,0.6)'
3726
+ } },
3727
+ React.createElement("div", { style: { fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: '#d1d4dc', display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
3728
+ "Indicators",
3729
+ React.createElement("div", { style: { display: 'flex', gap: '8px' } },
3730
+ React.createElement("button", { onClick: () => setShowConfigPanel(!showConfigPanel), style: { background: 'transparent', border: 'none', color: '#b2b5be', cursor: 'pointer', fontSize: '16px' } },
3731
+ React.createElement(BsGear, null)),
3732
+ React.createElement("button", { onClick: takeScreenshot, style: { background: 'transparent', border: 'none', color: '#b2b5be', cursor: 'pointer', fontSize: '16px' } },
3733
+ React.createElement(BsCamera, null)))),
3734
+ ['SMA', 'EMA', 'RSI', 'MACD'].map(indicatorName => {
3735
+ const isActive = indicators.some(config => config.name === indicatorName && config.visible);
3736
+ return (React.createElement("div", { key: indicatorName, style: { display: 'flex', alignItems: 'center', marginBottom: '8px' } },
3737
+ React.createElement("input", { type: "checkbox", checked: isActive, onChange: (e) => {
3738
+ if (e.target.checked) {
3739
+ const newConfig = {
3740
+ name: indicatorName,
3741
+ params: indicatorName === 'SMA' ? { period: 20 } :
3742
+ indicatorName === 'EMA' ? { period: 20 } :
3743
+ indicatorName === 'RSI' ? { period: 14 } :
3744
+ { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
3745
+ color: '#2962ff',
3746
+ visible: true
3747
+ };
3748
+ addIndicator(newConfig);
3749
+ }
3750
+ else {
3751
+ removeIndicator(indicatorName);
3752
+ }
3753
+ }, style: { marginRight: '8px' } }),
3754
+ React.createElement("span", { style: { fontSize: '12px', color: '#b2b5be' } }, indicatorName)));
3755
+ }))),
3756
+ showConfigPanel && (React.createElement("div", { style: {
3757
+ position: 'absolute',
3758
+ top: '50px',
3759
+ right: '280px',
3760
+ background: activeTheme.panelBg,
3761
+ border: `1px solid ${activeTheme.panelBorder}`,
3762
+ borderRadius: '12px',
3763
+ padding: '16px',
3764
+ minWidth: '240px',
3765
+ maxHeight: '60vh',
3766
+ overflowY: 'auto',
3767
+ zIndex: 1000,
3768
+ boxShadow: '0 8px 24px rgba(0,0,0,0.6)'
3769
+ } },
3770
+ React.createElement("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '12px', color: activeTheme.textPrimary } }, strings.config.title),
3771
+ React.createElement("div", { style: { marginBottom: '12px' } },
3772
+ React.createElement("label", { style: { fontSize: '12px', color: activeTheme.textSecondary, display: 'block', marginBottom: '4px' } }, strings.config.language),
3773
+ React.createElement("select", { value: language, onChange: (e) => setLanguage(e.target.value), style: { width: '100%', background: activeTheme.plotBg, color: activeTheme.textPrimary, border: `1px solid ${activeTheme.panelBorder}`, padding: '6px', borderRadius: '8px' } },
3774
+ React.createElement("option", { value: "es" }, "Espa\u00F1ol"),
3775
+ React.createElement("option", { value: "en" }, "English"))),
3776
+ React.createElement("div", { style: { marginBottom: '12px' } },
3777
+ React.createElement("label", { style: { fontSize: '12px', color: activeTheme.textSecondary, display: 'block', marginBottom: '4px' } }, strings.config.colors),
3778
+ React.createElement("select", { value: colorScheme, onChange: (e) => setColorScheme(e.target.value), style: { width: '100%', background: activeTheme.plotBg, color: activeTheme.textPrimary, border: `1px solid ${activeTheme.panelBorder}`, padding: '6px', borderRadius: '8px' } },
3779
+ React.createElement("option", { value: "green" }, strings.config.colorOptions.green),
3780
+ React.createElement("option", { value: "red" }, strings.config.colorOptions.red))),
3781
+ React.createElement("div", { style: { marginBottom: '12px' } },
3782
+ React.createElement("label", { style: { fontSize: '12px', color: activeTheme.textSecondary, display: 'block', marginBottom: '6px' } }, "Theme preset"),
3783
+ React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, themeOptions.map(option => (React.createElement("button", { key: option.key, onClick: () => setThemePreset(option.key), style: {
3784
+ padding: '6px 10px',
3785
+ borderRadius: '999px',
3786
+ border: `1px solid ${themePreset === option.key ? activeTheme.accent : activeTheme.panelBorder}`,
3787
+ background: themePreset === option.key ? activeTheme.accentSoft : 'transparent',
3788
+ color: activeTheme.textPrimary,
3789
+ fontSize: '11px',
3790
+ cursor: 'pointer'
3791
+ } }, option.label))))),
3792
+ themePreset === 'custom' && (React.createElement("div", { style: { marginBottom: '12px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '10px' } }, CUSTOM_THEME_FIELDS.map(field => (React.createElement("label", { key: field.key, style: { fontSize: '11px', color: activeTheme.textSecondary, display: 'flex', flexDirection: 'column', gap: '4px' } },
3793
+ React.createElement("span", null, field.label),
3794
+ React.createElement("input", { type: "color", value: customTheme[field.key], onChange: (event) => handleCustomThemeChange(field.key, event.target.value), style: { width: '100%', height: '34px', border: 'none', cursor: 'pointer', borderRadius: '8px', background: 'transparent' } })))))),
3795
+ React.createElement("div", { style: { fontSize: '12px', color: activeTheme.textSecondary } }, strings.config.soon))),
3796
+ React.createElement("div", { style: { padding: isMobile ? '8px 16px' : '10px 20px', background: activeTheme.panelBg, borderTop: `1px solid ${activeTheme.panelBorder}`, display: 'flex', flexWrap: 'wrap', gap: isMobile ? '12px' : '18px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: activeTheme.textSecondary } },
3797
+ React.createElement("span", null,
3798
+ "Latency ",
3799
+ latencyMs,
3800
+ "ms"),
3801
+ React.createElement("span", null,
3802
+ "Session ",
3803
+ derivedStats?.session ?? 'Global'),
3804
+ React.createElement("span", null, "Feed Binance Composite"),
3805
+ React.createElement("span", null, "Security AES-256"))));
3806
+ };
3807
+ var TradingViewChart_default = React.memo(TradingViewChart);
3808
+
3809
+ exports.Chart = Chart;
3810
+ exports.TradingViewChart = TradingViewChart_default;
3811
+ //# sourceMappingURL=index.cjs.map